【界面设计器】自定义字段组件实战——千分位输入框

阅读之前

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

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

业务背景

用户在输入【金额】字段时,实时展示千分位格式。

业务分析

从需求来看,我们需要实现一个【千分位】组件,并且该组件允许在【表单】视图中使用。

扩展实现:该组件虽然仅要求在【表单】中使用,但也可以在【搜索】中使用完全相同的实现,因此这里我们在注册时会增加【搜索】视图,将【千分位】组件应用在【搜索】中。对于【表格】、【详情】和【画廊】来说,该组件是没有【输入】行为的展示组件,在这里我们不进行演示。

准备工作

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

业务模型定义

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

名称 API名称 业务类型 是否多值 长度(单值长度)
编码 code 文本 128
名称 name 文本 128
金额 money 金额 -

创建组件、元件

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

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

创建千分位组件

image.png

创建千分位元件

image.png

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

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

关键点详解

数据交互类型的字段组件(以下简称展示组件)与仅展示类型的字段组件(以下简称交互组件)有一些差别。

通常情况下,在展示组件中仅需使用value属性即可展示相关内容。在交互组件中除了value用于展示外,还需使用changefocus以及blur处理用户输入时的基本交互逻辑。

数据交互方法主要功能说明:

  • change方法:当值发生变更时调用,根据字段相关配置将值回填至表单中。
  • focus方法:当组件获取焦点时调用,记录当前值,并在调用blur方法时进行处理。
  • blur方法:当前组件失去焦点时调用,根据focus方法记录的值,进行失焦触发逻辑的执行。

这里的三个数据交互方法仅仅是对用户行为的抽象,而并非限定其使用场景。

通常来说,这三个方法的调用顺序为:focus --> change --> blur

如在日期时间组件中,面板打开时调用了focus方法,面板值发生变更时,调用了change方法,面板关闭时调用了blur方法。

如在地图组件中,选中地图上的某个点时仅会调用change方法,用户交互上并不能体现出focusblur的行为。因此对于这个组件而言,只会有change方法被执行。

代码实现参考

PS:oio-input-number样式必须升级到4.6.x以上的最新版本才支持

Thousandth.vue
<template>
  <a-input-number
    class="oio-input-number"
    :value="inputValue"
    :formatter="formatter"
    :parser="parser"
    @update:value="change"
    @focus="focus"
    @blur="blur"
  />
</template>
<script lang="ts">
import { InputNumber as AInputNumber } from 'ant-design-vue';
import { computed, defineComponent } from 'vue';

export default defineComponent({
  name: 'Thousandth',
  components: {
    AInputNumber
  },
  props: {
    value: {
      type: [String, Number]
    },
    change: {
      type: Function
    },
    focus: {
      type: Function
    },
    blur: {
      type: Function
    }
  },
  setup(props) {
    const inputValue = computed(() => {
      return props.value;
    });

    const formatter = (value) => {
      return `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
    };

    const parser = (value) => {
      return value.replace(/\$\s?|(,*)/g, '');
    };

    return {
      inputValue,
      formatter,
      parser
    };
  }
});
</script>
FormMoneyThousandthFieldWidget.ts
import { FormFieldWidget, ModelFieldType, SPI, ViewType } from '@kunlun/dependencies';
import Thousandth from './Thousandth.vue';

@SPI.ClassFactory(
  FormFieldWidget.Token({
    viewType: [ViewType.Form, ViewType.Search],
    ttype: ModelFieldType.Currency,
    widget: 'Thousandth',
    multi: false
  })
)
export class FormMoneyThousandthFieldWidget extends FormFieldWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(Thousandth);
    return this;
  }
}

Oinone社区 作者:张博昊原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/6787.html

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

Like (0)
张博昊's avatar张博昊数式管理员
Previous 2024年4月19日 pm7:43
Next 2024年4月19日 pm8:51

相关推荐

  • 如何提高自定义组件的开发效率

    引言 本文将通过前端的开发者模式带领大家提高自定义组件的开发效率 支持2024年9月6日之后用npm i安装的4.7.x之后的所有版本 介绍前端开发者模式 开发者模式的特性 浏览器控制台可以看到更多利于开发的日志输出 页面顶部状态栏消息模块的轮询接口,将只在页面刷新后请求一次,这样会减少开发阶段不必要的请求,以及解决后端断点调试的时候被消息轮询干扰的问题 页面视图的数据将不在走缓存,而是每次实时从base_view表里查询template字段获取,方便需要实时看通过xml修改的页面效果 如何进入开发者模式 通过在浏览器地址栏后追加;mode=dev刷新页面进入开发者模式,页面加载后可以在浏览器开发者工具的应用标签下的本地存储空间(即localStorage)中看到本标识的存储,如果想退出开发者模式可以手动清除该值 开发者模式的应用场景 1.通过控制台跳转到视图动作(ViewAction)的调试页面 页面刷新后,可以在浏览器开发者工具的控制台看到如下图的日志输出,直接点击该链接可以跳转到当前页面的调试链接弹窗视图动作、抽屉视图动作等以弹出层为展示效果的页面,之前的版本是无法进入调试链接查看的,现在可以在弹窗等视图动作点击后在控制台看到该链接点击进入调试页面 2.查看页面母版(mask)、布局(layout)的匹配情况 母版(mask)匹配结果布局(layout)匹配结果 3.查看视图组件(View)、字段组件(Field)、动作组件(Action)的匹配情况 通过浏览器开发者工具的控制台,我们可以看到每个视图、字段、字段匹配到的组件的class类名,这样我们就知道当前用的是那个组件,在自定义的时候可以在源码查看该组件类的实现代码,然后再选择是否继承该父类。另外我们在给组件定义SPI条件的时候,可以参考匹配入参和匹配结果内的属性。 匹配入参是当前元数据SPI的最全量注册条件 匹配结果是当前匹配到的组件的注册条件,一般情况下我们只需要在匹配结果的条件加上当前模型编码(model)、视图名称(viewName)、字段名称(name)、动作名称(actionName)等条件就可以完成自定义组件的覆盖。 在浏览器开发者工具的控制台,通过模型编码、视图名称、字段名称、动作名称等关键字快速搜索定位到需要查找的组件 控制台搜索快捷键:win系统通过ctrl+f,mac系统通过cmd+f 总结 在确保自定义部分代码最终正确被import导入到启动工程main.ts内的情况下,可以通过开发者模式在浏览器控制台打印的日志来排查组件未正确覆盖的问题。

    2024年9月6日
    2.0K00
  • 前端自定义左树右表中的树

    在 oinone 平台中,提供了默认的左树右表的视图,用户可以通过界面设计器配置,默认的树视图不一定满足所有需求,尤其当需要自定义功能或复杂的交互时,我们可以通过自定义视图来实现更灵活的展现。 本文将带你一步步了解如何自定义左树右表视图中的树组件。 自定义树视图 1. 使用界面设计器配置视图 首先,我们需要通过界面设计器生成基础的左树右表视图。界面设计器允许用户根据不同需求进行拖拽配置,快速创建可视化界面。 配置完视图之后,我们可以重写左侧的树组件。Oinone 的默认树组件是 TableSearchTreeWidget,通过自定义的方式,我们可以实现更高级的功能。 2. 重写 TableSearchTreeWidget import { BaseElementWidget, SPI, TableSearchTreeWidget, ViewType } from '@kunlun/dependencies'; import CustomTableSearchTree from './CustomTableSearchTree.vue'; @SPI.ClassFactory( BaseElementWidget.Token({ viewType: [ViewType.Table, ViewType.Form], widget: 'tree', model: 'resource.k2.Model0000000100' // 改成自己的模型 }) ) export class CustomTableSearchTreeWidget extends TableSearchTreeWidget { public initialize(props) { super.initialize(props); this.setComponent(CustomTableSearchTree); return this; } } 3. 定义 Vue 树组件 接下来,我们来实现 CustomTableSearchTree.vue 组件。这个组件将处理树的数据加载、节点选中等逻辑。你可以根据项目的需要修改其中的交互逻辑或 UI 设计。 <template> <a-tree :load-data="onLoadData" :tree-data="treeData" @select="onSelected" /> </template> <script lang="ts"> import { OioTreeNode, TreeUtils } from '@kunlun/dependencies'; import { computed, defineComponent } from 'vue'; export default defineComponent({ props: { rootNode: { type: Object }, loadData: { type: Function, required: true }, onSelected: { type: Function, required: true } }, setup(props) { // // 计算树的数据源,使用 TreeUtils 处理 const treeData = computed(() => { return TreeUtils.fillLoadMoreAction([…(props.rootNode?.children || [])]); }); // 异步加载子节点 const onLoadData = async (node) => { return await props.loadData(node.dataRef); }; // 处理节点选中事件 const onSelected = ( selectedKeys: string[], e: { nativeEvent: PointerEvent; node: { dataRef: OioTreeNode }; selected: boolean } ) => { props.onSelected?.(e.node.dataRef, e.selected); }; return { treeData, onLoadData, onSelected }; } }); </script> 4. 自定义…

    2024年10月21日
    3.1K00
  • 前端自定义组件之单页面步骤条

    本文将讲解如何通过自定义,实现单页面的步骤条组件。其中每个步骤的元素里都是界面设计器拖出来的。 实现路径 整体的实现思路是界面设计器拖个选项卡组件,自定义这个选项卡,里面的每个选项页都当成一步渲染出来,每一步的名称是选项页的标题。 1. 界面设计器拖出页面 我们界面设计器拖个选项卡组件,然后在每个选项页里拖拽任意元素。完成后点击右上角九宫格,选中选项卡,填入组件 api 名称,作用是把选项卡切换成我们自定义的步骤条组件,这里的 api 名称和自定义组件的 widget 对应。最后发布页面,并绑定菜单。 2. 组件实现 widget 组件重写了选项卡,核心函数 renderStep,通过 DslRender.render 方法渲染界面设计器拖拽的元素,每一步的 step 又是解析选卡页得到的。 import { SPI, Widget, DefaultTabsWidget, BasePackWidget, DslDefinition, DslRender, DslDefinitionType, CallChaining, customMutation } from '@oinone/kunlun-dependencies'; import { VNode } from 'vue'; import NextStepSinglePage from './NextStepSinglePage.vue'; @SPI.ClassFactory(BasePackWidget.Token({ widget: 'NextStepSinglePage' })) export class NextStepSinglePageWidget extends DefaultTabsWidget { public initialize(props) { super.initialize(props); this.setComponent(NextStepSinglePage); return this; } @Widget.Reactive() public get invisible() { return false; } // 配置的每一步名称,解析选项页的标题 @Widget.Reactive() public get titles() { return this.template?.widgets?.map((item) => item.title) || []; } // region 上一步下一步配置 // 步骤数组,数组里的元素即步骤要渲染的内容 @Widget.Reactive() public get steps(): DslDefinition[] { // 每个 tab 是一个步骤,这里会有多个步骤 // 每个步骤里有多个元素,又是数组 // 所以这里是二维数组 const tabDsls: DslDefinition[][] = this.template?.widgets.map((item) => item.widgets) || []; // 对每个步骤的子元素们,外侧包一层 row 布局,所以变回了一维数组 return tabDsls.map((tabDsl) => { return { …(this.template || {}), dslNodeType: DslDefinitionType.PACK, widgets: tabDsl, widget: 'row', resolveOptions: { mode: 1 } }; }); } // 渲染步骤,每个步骤有多个子元素 @Widget.Method() public renderStep(step: DslDefinition): VNode | undefined { return DslRender.render(step); } // region 校验相关 // 校验的钩子 @Widget.Reactive() @Widget.Inject('validatorCallChaining') protected parentValidatorCallChaining: CallChaining<boolean> | undefined; // 校验步骤表单 @Widget.Method() public async onValidator(): Promise<boolean> { const res = await this.parentValidatorCallChaining?.syncCall(); return res…

    2025年7月8日
    88800
  • 「前端」动作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日
    1.5K00
  • 从一个方法开始:浅析页面渲染流程

    渲染前的准备 渲染前的准备,在 Vue 渲染框架下,会先安装所有所支持的默认组件,比如 Mask,Header 等,这些组件支持 XML 默认模版的 Vue 框架下的渲染,详情可见 main.ts 中,maskInstall 与 install,这两个函数将平台内部支持的组件进行了注册,随后将整个 Vue 挂载为运行时 App,随后进行初始化。 渲染的起步 OioProvider 方法是整个应用的入口,我们忽略掉一些配置方法,将注意力集中到 initializeServices 从名称中我们可以看出来内部保存的都是初始化服务,其中提供了渲染服务等,我们当前使用的是 Vue 框架,所以当前其渲染的 Root 节点为 Vue, 以此,我们视野可以暂时转移到 admin-base中的 Root.vue以及 RootWidget上, 其实现了整个 Vue 框架下的 Root 节点如何渲染,其中定义了多个 widget,比如登陆页,首页,忘记密码已经重置密码等页面, 在本文中我们着重关注渲染首页的能力,RootWidget将 DefaultMetadataMainViewWidget作为渲染 Props中的 page即首页提供给下层组件使用, 渐入佳境 DefaultMetadataMainViewWidget从名称中可以看到,其为元数据主视图,主要做两件事,1:提供 Mask 的渲染2:提供元数据上下文初始化 该组件主要通过观察路由变化触发上面两个动作,当路由发生变化,该组件 reloadPage将被调用,reloadPage方法通过组装路由参数构成一个唯一 key 向后段查询当前路由所对应的渲染信息, 随后将获取到的信息进行处理,初始化,即 元数据上下文初始化,在初始化后,会将获取到的数据注入到 MetadataViewWidget中, Mask 的渲染 关于 Mask 的渲染,在获取到数据后,将生成 maskTemplate,并将其赋值, DefaultMetadataMainView.vue文件将渲染该模板,并渲染到页面中 数据的变更 当上面两件任务完成后,将开始主视图的渲染,上文提到,DefaultMetadataMainViewWidget只负责 mask 的渲染和上下文的初始化,所以 DefaultMetadataMainViewWidget通过触发事件的方式来实现主视图的渲染, DefaultMetadataMainViewWidget将必要信息作为事件参数触发,MultiTabsContainerWidget接收到 reloadMainViewCallChaining事件后,开启主视图渲染, MultiTabsContainerWidget会刷新运行时上下文,即 refreshRuntimeContext,该方法将尝试查询并通过 createOrUpdateEnterTab方法创建 Tab 页,createOrUpdateEnterTab最终生成一个 MultiTabItem格式的对象,该对象描述了 Tab 的相关信息,随后调用 createTabContainerWidget创建 tab 的容器即新建了一个 MultiTabContainerWidget组件即单个 tab 的容器,随后调用 setActiveTabItem, 并获取其绑定的 Vue 组件,并将其组件放置在 KeepAlive内部,触发更新, 主视图的渲染 MultiTabContainerWidget继承自MetadataViewWidget,当 MetadataViewWidget数据发生变更, 其绑定的 Vue 组件将解析 viewTemplate, 获取到与该模板 dslNodeType想匹配的 Vue 组件,当前例子中为 View.vue,随后 View.vue开始渲染,View.vue文件只是一个纯粹的容器,当 View.vue被挂载时,其内部的 template属性包含了整个页面的描述信息,View.vue需要做的就是将这个 template翻译并渲染成 DOM 展现在浏览器上, 渲染整个页面 当 View.vue被挂载时,其内部的 template属性包含了整个页面的描述信息,View.vue主要做了两个事情,一:将 template 中的 widget转换为组件,二:根据当前的 template信息生成 slot信息, const currentSlots = computed<Slots | undefined>( () => DslRender.fetchVNodeSlots(props.dslDefinition) || (Object.keys(context.slots).length ? context.slots : undefined) ); const renderWidget = createCustomWidget(InternalWidget.View, { …context.attrs, type: props.type || viewType.value, template: props.dslDefinition, metadataHandle: props.metadataHandle || metadataHandle.value, rootHandle: props.rootHandle || rootHandle.value, parentHandle: props.parentHandle || parentHandle.value, slotName: props.slotName, inline: inlineProp } as ViewWidgetProps); 生成这两部分信息后,View.vue会将这两部分挂载到页面上,这两部分从代码中可以看出,主要靠 fetchVNodeSlots,createCustomWidget两个函数, export function createCustomWidget( widget: string, props: CustomWidgetProps ): RenderWidget | undefined public static fetchVNodeSlots(dsl: DslDefinition | undefined, supportedSlotNames?: string[]):…

    2025年3月19日
    85500

Leave a Reply

Please Login to Comment