自定义组件之手动渲染基础(v4)

阅读之前

你应该:

为什么需要手动渲染

自定义组件之自动渲染(组件插槽的使用)(v4)文章中,我们介绍了带有具名插槽的组件可以使用DSL模板进行自动化渲染,并且可以用相对简单的方式与元数据进行结合。

虽然自动化渲染在实现基本业务逻辑的情况下,有着良好的表现,但自动化渲染方式也有着不可避免的局限性。

比如:当需要多个视图在同一个位置进行切换

在我们的平台中,界面设计器的设计页面,在任何一个组件在选中后,需要渲染对应的右侧属性面板。每个面板的视图信息是保存在对应的元件中的。根据元件的不同,找到对应的视图进行渲染。在单个视图中使用自动化渲染是无法处理这一问题的,我们需要一种可以局部渲染指定视图的方式,来解决这一问题。

获取一个视图

使用ViewCache获取视图
export class ViewCache {
  /**
   * 通过模型编码和名称获取视图
   * @param model 模型编码
   * @param name 名称
   * @param force 强制查询
   * @return 运行时视图
   */
  public static async get(model: string, name: string, force = false): Promise<RuntimeView | undefined>

  /**
   * 通过模型编码、自定义名称和模板获取编译后的视图(此视图非完整视图,仅用于自定义渲染使用)
   * @param model 模型编码
   * @param name 名称(用作缓存key)
   * @param template 视图模板
   * @param force 强制查询
   * @return 运行时视图
   */
  public static async compile(
    model: string,
    name: string,
    template: string,
    force = false
  ): Promise<RuntimeView | undefined>
}
  • ViewCache#get:用于服务端定义视图,客户端直接获取完整视图信息。
  • ViewCache#compile:用于客户端定义视图,通过服务端编译填充元数据相关信息,但不包含视图其他信息。

自定义一个带有具名插槽的组件,并提供切换视图的相关按钮

以下是一个自定义组件的完整示例,其使用ViewCache#compile方法获取视图。

view.ts
const template1 = `<view>
    <field data="id" invisible="true" />
    <field data="code" label="编码" />
    <field data="name" label="名称" />
</view>`;

const template2 = `<view>
    <field data="id" invisible="true" />
    <field data="name" label="名称" />
    <field data="code" label="编码" />
</view>`;

export const templates = {
  template1,
  template2
};
ManualDemoWidget.ts
import {
  BaseElementWidget,
  createRuntimeContextForWidget,
  FormWidget,
  RuntimeView,
  SPI,
  ViewCache,
  ViewType,
  Widget
} from '@kunlun/dependencies';
import ManualDemo from './ManualDemo.vue';
import { templates } from './view';

@SPI.ClassFactory(BaseElementWidget.Token({ widget: 'ManualDemo' }))
export class ManualDemoWidget extends BaseElementWidget {
  private formWidget: FormWidget | undefined;

  public initialize(props) {
    super.initialize(props);
    this.setComponent(ManualDemo);
    return this;
  }

  @Widget.Method()
  public async resetTemplate(key: string): Promise<void> {
    // 获取运行时视图
    const view = await this.fetchViewByCompile(key);
    if (!view) {
      console.error('Invalid view');
      return;
    }

    // 销毁旧组件
    this.formWidget?.dispose();
    this.formWidget = undefined;

    // 创建运行时上下文
    const runtimeContext = createRuntimeContextForWidget(view);
    const runtimeContextHandle = runtimeContext.handle;

    // 获取初始化数据
    const formData = await runtimeContext.getInitialValue();

    // 提交数据源到当前视图
    this.reloadDataSource(formData);
    this.reloadActiveRecords(formData);

    // 创建所需组件
    this.formWidget = this.createWidget(FormWidget, 'formWidget', {
      metadataHandle: runtimeContextHandle,
      rootHandle: runtimeContextHandle,
      template: runtimeContext.viewTemplate,
      inline: true
    });
  }

  public async fetchViewByCompile(key: string): Promise<RuntimeView | undefined> {
    const template = templates[key];
    if (!template) {
      console.error('error template', key);
      return;
    }

    // 模型编码
    const model = ${model};

    // 通过编译获取视图(非完整视图数据)
    const view = await ViewCache.compile(model, key, template);
    if (!view) {
      return;
    }

    // 补充视图类型
    view.type = ViewType.Form;
    return view;
  }

  protected mounted() {
    // 挂载时初始化指定视图
    this.resetTemplate('template1');
  }
}
ManualDemo.vue
<template>
  <div class="manual-demo-wrapper" v-show="!invisible">
    <oio-button @click="() => switchTemplate('template1')">切换模版1</oio-button>
    <oio-button @click="() => switchTemplate('template2')">切换模版2</oio-button>
    <div class="manual-demo-widget">
      <slot name="formWidget" />
    </div>
  </div>
</template>
<script lang="ts">
import { OioButton } from '@kunlun/vue-ui-antd';
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'ManualDemo',
  components: {
    OioButton
  },
  props: {
    invisible: {
      type: Boolean,
      default: undefined
    },
    resetTemplate: {
      type: Function
    }
  },
  setup(props) {
    const switchTemplate = (key: string) => {
      props.resetTemplate?.(key);
    };

    return {
      switchTemplate
    };
  }
});
</script>
视图DSL
<view type="FORM">
    <template slot="actionBar">
        <action name="create" label="创建" />
    </template>
    <template slot="form" widget="ManualDemo" />
</view>

在上述示例中,我们在view.ts定义了两个用于切换的视图模板,在视图DSL指定widget="ManualDemo"使用我们定义的组件,以此来实现我们的需求。

关键点详解

Class ComponentVue组件之间的交互

@Widget.Method()
public async resetTemplate(key: string): Promise<void> {
  ......
}
export default defineComponent({
  props: {
    resetTemplate: {
      type: Function
    }
  },
  setup(props) {
    const switchTemplate = (key: string) => {
      props.resetTemplate?.(key);
    };

    return {
      switchTemplate
    };
  }
});

使用@Widget.Method()装饰器,将resetTemplate方法通过props传递给Vue组件,通过Vue组件的用户行为(点击按钮)调用所需的执行逻辑。

在这个示例中,我们通过在setup中定义switchTemplate方法,来避免resetTemplate方法可能为造成的一些异常。在最佳实践中,我们通常不建议开发者直接使用props中定义的Function类型。在大多数情况下,在Class Component中定义的方法应该是尽可能抽象的,而不是为了某个Vue组件提供的特殊方法。

手动渲染常用步骤

  1. 获取运行时视图
  2. 销毁旧组件
  3. 创建运行时上下文
  4. 获取初始化数据
    • 模板中定义的初始化数据
    • 调用服务端接口获取
  5. 根据当前场景选择数据源持有者
    • 提交数据源到当前视图
    • 组件持有数据源
  6. 创建所需组件

一般情况下,我们仅需按照上述步骤就可以实现手动渲染功能。

特别的,我们为了保证组件的生命周期完整,并且在运行时不会出现异常挂载/卸载的情况,自动渲染手动渲染是不允许交叉使用的,并且根据组件所属的场景不同,手动渲染自动渲染更关注数据交互相关的内容。

有时可能会遇到数据交互相关的意外

  • 数据源无法共享导致的动作功能异常
  • 数据源未正确提交到所需组件,可能被某个具有数据源的组件拦截或穿透提交到更上层组件。

获取运行时视图

public async fetchViewByCompile(key: string): Promise<RuntimeView | undefined> {
  // 获取视图模板
  const template = templates[key];
  if (!template) {
    console.error('error template', key);
    return;
  }

  // 模型编码
  const model = ${model};

  // 通过编译获取视图(非完整视图数据)
  const view = await ViewCache.compile(model, key, template);
  if (!view) {
    return;
  }

  // 补充视图类型
  view.type = ViewType.Form;
  return view;
}

在这个示例中,我们通过客户端定义视图模板,传递给服务端进行编译得到运行时视图,并手动补充了视图类型。

但通常情况下,我们采用的是通过服务端定义视图模板,并根据上下文中提供的某些信息,动态获取这个视图的模型编码视图名称,并使用ViewCache#get方法获取的。

如下述方法定义:

public fetchView(model: string, name: string): Promise<RuntimeView | undefined> {
  return ViewCache.get(model, name);
}

创建运行时上下文

const runtimeContext = createRuntimeContextForWidget(view);
const runtimeContextHandle = runtimeContext.handle;
createRuntimeContextForWidget定义
/**
 * 为组件创建运行时上下文,一般用于直接创建组件的场景
 * 注:deep属性必须保持一致
 * @param view 运行时视图
 * @param options 创建可选项 默认值: mergeLayout = false, deep = true
 * @return 组件运行时上下文
 */
export function createRuntimeContextForWidget(
  view: RuntimeView,
  options?: { mergeLayout?: boolean; deep?: boolean }
): RuntimeContext

此处的运行时上下文是针对某个具体组件的模板进行创建的,它与其他通过自动化渲染方式获取的上下文有所不同。

一般的,我们将传入的运行时视图称为组件视图,由此生成的运行时上下文称为组件运行时上下文,将这个运行时上下文中获取的viewTemplate称为组件模板

template1为例:

<view>
    <field data="id" invisible="true" />
    <field data="code" label="编码" />
    <field data="name" label="名称" />
</view>

该模板的view标签并非真实渲染了一个视图组件,而是将其作为组件模板根标签,其标签名称在运行时是没有任何含义的。

对于上述创建的FormWidget组件来说,通过this.getDsl()方法获取的DslDefinition对象就是view标签

例如,我们将template1改为:

<view layout="horizontal">
    <field data="id" invisible="true" />
    <field data="code" label="编码" />
    <field data="name" label="名称" />
</view>

FormWidget组件中,通过this.getDsl().layout将获取到horizontal值。

创建所需组件

// 获取初始化数据
const formData = await runtimeContext.getInitialValue();

// 提交数据源到当前视图
this.reloadDataSource(formData);
this.reloadActiveRecords(formData);

// 创建所需组件
this.formWidget = this.createWidget(FormWidget, 'formWidget', {
  metadataHandle: runtimeContextHandle,
  rootHandle: runtimeContextHandle,
  template: runtimeContext.viewTemplate,
  inline: true
});

在示例中,我们将数据源提交到当前视图用于和其他组件共享数据源,并创建了一个平台提供的FormWidget组件。'formWidget'Vue组件中定义的插槽名称。

创建任何组件时,metadataHandlerootHandle template以及inline属性是必须的。

  • metadataHandlerootHandle:用于应用运行时上下文到当前组件。一般情况下,手动渲染组件的这两个值始终是保持一致的。
  • template:用于指定组件使用的组件模板
  • inline:用于决定当前组件是否与浏览器的URL进行交互。

获取组件模板的初始化数据

  • RuntimeContext#getDefaultValue:收集组件模板field标签定义的defaultValue属性,并返回一个对象
  • RuntimeContext#getInitialValue:在RuntimeContext#getDefaultValue方法的基础上,将field标签定义的relatedcompute属性进行了首次计算,并返回一个对象。(目前最新版本与RuntimeContext#getDefaultValue一致,但我们建议开发者使用该方法)

通常情况下,我们也需要调用服务端服务,来获取最终的初始化数据。在这里不做相关介绍。

根据当前场景选择数据源持有者

数据交互相关章节中,我们对数据源持有者有详细介绍,在这里不再赘述。

视图DSL定义中,我们使用ManualDemo组件的方式决定,我们在这里需要将数据源提交到当前视图,用于actionBar组件中定义的动作进行数据源共享。动作执行时的所需的数据源,就是我们这里提交给当前视图的数据源。

一般情况下,手动渲染组件的数据源不再通过组件的自动查询功能进行数据初始化,这又可能会导致数据交互流程变得难以理解。因此我们建议手动渲染组件的数据源总是在创建组件前就提前准备好的。

下面我们介绍如何让组件持有数据源,该方法并不适用于当前场景,但开发者有必要了解相关内容。

组件持有数据源
// 获取初始化数据
const formData = await runtimeContext.getInitialValue();

// 创建所需组件
this.formWidget = this.createWidget(FormWidget, 'formWidget', {
  metadataHandle: runtimeContextHandle,
  rootHandle: runtimeContextHandle,
  dataSource: formData,
  activeRecords: formData,
  template: runtimeContext.viewTemplate,
  inline: true
});

与之前不同的是,我们将dataSourceactiveRecords通过属性直接传递给组件实例即可。

在当前场景中,这样修改后的结果将导致页面中的按钮无法获取数据源,无法点击。

支持手动渲染的组件

在平台提供的所有组件中,并非所有组件都支持手动渲染。包括但不限于如下几个组件:

  • TableWidget:表格组件
  • FormWidget:表单组件
  • DetailWidget:表单组件(详情)

如何让组件支持手动渲染

支持手动渲染的组件与普通组件唯一的不同就是对metadataHandlerootHandle的处理。

Vue框架下,我们提供了mixin混入的方式,让开发者可以更简单的定义一个支持手动渲染的组件。

组件注册、组件定义都无需改变,仅需在对应的Vue组件中添加ManualWidget混入即可。如下所示:

<script lang="ts">
import { ManualWidget } from '@kunlun/dependencies';
import { defineComponent } from 'vue';

export default defineComponent({
  ......
  mixins: [ManualWidget],
  ......
});
</script>

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

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

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

相关推荐

  • 如何自定义指定页面的样式

    可以通过在layout上给页面元素加css的class来解决此问题 import { registerLayout, ViewType } from '@kunlun/dependencies'; export const install = () => { registerLayout( ` <!– 给视图加class –> <view type="FORM" class="my-form-view"> <!– 给动作条组件加class –> <element widget="actionBar" slot="actionBar" class="my-action-bar" slotSupport="action" > <xslot name="actions" slotSupport="action" /> </element> <!– 给表单组件加class –> <element widget="form" slot="form" class="my-form-widget"> <xslot name="fields" slotSupport="pack,field" /> </element> </view> `, { viewType: ViewType.Form, // 页面的模型编码,可在浏览器地址栏的model=xxxx获取 model: 'resource.k2.Model0000000109', // 页面的动作名称,可在浏览器地址栏的action=xxxx获取 actionName: 'uiViewb2de116be1754ff781e1ffa8065477fa' } ); }; install(); 这样我们就可以在浏览器的html标签中查看到给组件加的class,通过加上这个作用域,可以给当前页面写特定样式

    2024年8月16日
    1.1K00
  • 如何提高自定义组件的开发效率

    引言 本文将通过前端的开发者模式带领大家提高自定义组件的开发效率 支持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日
    1.7K00
  • TableWidget 与 FormWidget 浅析

    前言:本文主要聚集于 TableWidget和 FormWidget,对两者以外的内容不做赘述。 TableWidget和 FormWidget作为一个基本的渲染模块与 Mask等不同,TableWidget与 FormWidget有着明确的目的,比如 TableWidget就是作为一个表格视图,这种名称中可以看得出来。其将表格的一系列能力聚拢,如单元格,行列选择排序,翻页等等; TableWidget 我们使用表格到底是在使用什么? 在我们讲述渲染流程前,我们需要提一个问题,在我们使用表格组件时,我们在使用什么,换而言之,我们对表格组件中关注的是什么?应该说我们更关注的是表格所展示的数据。TableWidget或者说整个低代码其实解决的就是这个问题,让我们可以很方便的展示数据。不用关心一些细枝末节 TableWidget 的渲染 TableWidget在整个渲染流程中的负责组装各种 Vue 组件所需要的核心数据或事件回调并传递给其绑定的 Vue 组件即 DefaultTable,如 allowRowClick,rowClickMode等等,这些值会作为 Props 传递给 DefaultTable,DefaultTable则一定意义上充当了一个桥接层,主要做了两件事,处理 Props, 处理 Slot,TableWidget传递给 DefaultTable的 Props 会经过 DefaultTable再次组合,创建新的 Props,同时根据当前的 Props 判断是否有必要新增一些 slot,比如 OioPagination组件是否需要渲染就取决于 Props.showPagination 的值,经过 DefaultTable的处理后, props与 slot会进一步交给 OioTable进行渲染,OioTable则会进一步聚合处理,比如如果没有 defaultSlot则进行空值的渲染,如果存在 footSlot则会构建一个包裹层去包裹它。这些组件协同完成了一个表格的结构,而我们真正关心的数据则由一个个 BaseTableColumnWidget渲染。BaseTableColumnWidget的渲染过程类似于 TableWidget,其负责组装 DefaultTableColumn渲染所需要的 props,然后交给 OioTableColumn进行实际渲染。并且会有多种基于 BaseTableColumnWidget的 widget通过重写 renderDefaultSlot,renderEditSlot,renderContentSlot,renderHeaderSlot等几个 props 实现不同状态,不同类型等组件的渲染。通过 TableWidget与 BaseTableColumnWidget相结合, Table 页面完成了主体框架与内容的渲染。而当数据完成渲染后,不可避免的会有数据交互,比如分页,排序,过滤等,这些交互都由 TableWidget与 BaseTableColumnWidget共同完成。比如排序翻页等,TableWidget会将事件作为 props 向下注入,当事件被调用,TableWidget会进行处理,比如发起请求,或者对内部数据排序等。而除了数据的展示,还有一些动作,即 Action ,Action 被触发时会按照内部的配置进行工作,比如编辑时,将获取 activeRecord,随后触发 Table 的编辑。 TableWidget就是这样去渲染出了一个完整的表格页面。能够完成数据的增删改查等操作 FormWidget FormWidget与 TableWidget一样,也是作为一个渲染模块,但是它与 TableWidget不同的是,FormWidget是作为一个表单视图,其将表单的一系列能力聚拢,如表单提交,表单校验,表单重置等等。其重点在于对数据的增改。所以在提供的能力上也有区别,比如 FormWidget没有提供翻页,排序,过滤等能力,因为这些能力属于表格的能力,而 FormWidget则更关注于表单的能力。提供了数据的存储,提交,校验等能力 Form 在渲染时流程与 Table 大同小异,其同样为三层结构 FormWidget => DefaultForm => FormFieldWidget 一层层向下渲染,不同的在于 FormWidget 更多的关注点在于维护其内部的 FormData 这是整个表单页面所围绕的东西,当页面上的控件发生变化,其变更的值会被收集到 FormData 中,并在后续中使用。同时在编辑已有数据场景下,Form 会将数据加载到 FormData,随后下放给 FormFiledWidget。 异同之处 从介绍中可以看出,Table 侧重于数据的查询展示,Form 则侧重于数据的变动处理,但是抽象的看其核心其实是同一套逻辑,即数据的存储与展示,中间或许会有对数据的某些处理,但是并不是本质上的区别,两者在核心理念上是一致的,即让使用者只需要关心数据本身,而不需要关注于繁琐的视图构造,这是整个低代码甚至前端的发展方向。

    2025年3月25日
    69900
  • 如果让表单支持单选 ?

    本文将介绍在代码和XML配置中的修改,支持表格的单选功能。 先自定义一个widget,继承平台默认的table @SPI.ClassFactory( BaseElementWidget.Token({ viewType: ViewType.Table, widget: 'MyRadioTable' }) ) export class MyRadioTable extends TableWidget { @Widget.Reactive() protected get selectMode(): ListSelectMode { return ListSelectMode.radio; } } 注册layout 如果当前是主表格,那么xml配置如下 const xml = `<view type="TABLE"> <pack widget="group"> <view type="SEARCH"> <element widget="search" slot="search" slotSupport="field" /> </view> </pack> <pack widget="group" slot="tableGroup"> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> <element widget="MyRadioTable" slot="table" slotSupport="field"> <element widget="expandColumn" slot="expandRow" /> <element widget="RadioColumn" /> <xslot name="fields" slotSupport="field" /> <element widget="rowActions" slot="rowActions" slotSupport="action" /> </element> </pack> </view>` registerLayout(xml, { moduleName: '模块名称', model: '模型', viewType: ViewType.Table }) 如果当前的表格是form表单页面的表格,那么xml配置如下 const xml = `<view type="TABLE"> <view type="SEARCH"> <element widget="search" slot="search" slotSupport="field" /> </view> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> <element widget="MyRadioTable" slot="table"> <element widget="expandColumn" slot="expandRow" /> <element widget="RadioColumn" /> <xslot name="fields" slotSupport="field" /> <element widget="rowActions" slot="rowActions" /> </element> </view>` registerLayout(xml, { moduleName: '模块名称', model: '模型', viewType: ViewType.Table })

    2023年11月27日
    85100
  • 自定义字段的数据联动

    某种情况下,开发人员期望自定以的字段发生变化后,需要修改其他的字段,这篇文章从两个维度来讲解如果处理数据的联动 界面设计器配置 1: 在界面设计器页面中的的组件区域找到自定义的字段,设计元件 2: 在模型区域,搜索提交方式,如果找到了,就把该字段拖拽进来, 如果找不到,就在元件中的组件区域,拖拽一个文本字段,按照下面的配置进行配置,然后保存 图一是找到了对应的字段图二是找不到对应的字段 【图一】 【图二】 图二的字段编码必须是constructDataTrigger 3: 从模型区搜索联动函数,将其拖拽进来 3: 从模型区搜索提交数据,将其拖拽进来4: 从模型区搜索提交字段,将其拖拽进来 5: 发布 (记得刷新页面哦) 最后再到对应的设计器页面,选中该字段,进行配置 提交方式为blur或者change , 需要开发者手动调用该方法 this.blur()或者this.change(value) // 字段对应的ts文件 class MyField extends FormFieldWidget { onChangeValue(val) { // this.change(val) // this.blur() } } 联动函数就是要调用的后端函数 提交数据分为:变更字段 -> 发生变化后的字段当前视图字段 -> 当前视图所有的字段指定字段 -> 指定字段,如果配置的指定字段,那么提交字段的配置就要输入对应的字段 代码配置 平台也支持通过代码的方式修改字段 // 字段对应的ts文件 class MyField extends FormFieldWidget { onChangeValue(val) { // 修改字段本身的值 this.change(val) // 修改其他字段的值 this.formData.otherField = 'value' this.reloadFormData$.subject.next(true); } }

    2023年11月9日
    1.7K00

Leave a Reply

登录后才能评论