前端页面嵌套

我们可能会遇到这些需求,如:页面中的一对多字段不是下拉框,而是另一个模型的表单组;页面中的步骤条表单,每一步的表单都需要界面设计器设计,同时这些表单可能属于不同模型。
这时候我们就可以采取页面嵌套的方式,在当前页面中,动态创建一个界面设计器设计的子页面。以一对多字段,动态创建表单子页面举例,以下是代码实现和原理分析。

代码实现

AddSubformWidget 动态添加表单 ts 组件

import {
  ModelFieldType,
  ViewType,
  SPI,
  BaseFieldWidget,
  Widget,
  FormO2MFieldWidget,
  ActiveRecord,
  CallChaining,
  createRuntimeContextByView,
  queryViewDslByModelAndName,
  uniqueKeyGenerator
} from '@oinone/kunlun-dependencies';
import { MyMetadataViewWidget } from './MyMetadataViewWidget';
import { watch } from 'vue';
import AddSubform from './AddSubform.vue';

@SPI.ClassFactory(
  BaseFieldWidget.Token({
    viewType: ViewType.Form,
    ttype: ModelFieldType.OneToMany,
    widget: 'AddSubform'
  })
)
export class AddSubformWidget extends FormO2MFieldWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(AddSubform);
    return this;
  }

  @Widget.Reactive()
  public myMetadataViewWidget: MyMetadataViewWidget[] = [];

  @Widget.Reactive()
  public myMetadataViewWidgetKeys: string[] = [];

  @Widget.Reactive()
  public myMetadataViewWidgetLength = 0;

  // region 子视图配置
  public get subviewModel() {
    return this.getDsl().subviewModel || 'clm.contractcenter.ContractSignatory';
  }
  public get subviewName() {
    return this.getDsl().subviewName || '签署方_FORM_uiViewa9c114903e104800b15e8f3749656b64';
  }

  // region 添加子视图块

  // 按钮添加点击事件
  @Widget.Method()
  public async onAddSubviewBlock() {
    const resView = await queryViewDslByModelAndName(this.subviewModel, this.subviewName);
    this.createDynamicSubviewWidget(resView);
  }

  // 创建子视图块
  public async createDynamicSubviewWidget(view, activeRecord: ActiveRecord = {}) {
    if (view) {
      // 根据视图构建上下文
      const runtimeContext = createRuntimeContextByView(
        {
          type: ViewType.Form,
          model: view.model,
          modelName: view.modelDefinition.name,
          module: view.modelDefinition.module,
          moduleName: view.modelDefinition.moduleName,
          name: view.name,
          dsl: view.template
        },
        true,
        uniqueKeyGenerator(),
        this.currentHandle
      );
      // 取得上下文唯一标识
      const runtimeContextHandle = runtimeContext.handle;

      const slotKey = `Form_${uniqueKeyGenerator()}`;

      // 创建子视图组件
      const widget = this.createWidget(new MyMetadataViewWidget(runtimeContextHandle),
        slotKey, // 插槽名
       {
        metadataHandle: runtimeContextHandle,
        rootHandle: runtimeContextHandle,
        mountedCallChaining: new CallChaining(),
        refreshCallChaining: new CallChaining(),
        inline: true,
        subIndex: this.myMetadataViewWidgetLength
      });
      widget.initContext(runtimeContext);
      // 初始化数据
      widget.setData(activeRecord);

      this.myMetadataViewWidgetLength++;
      this.myMetadataViewWidget.push(widget);
      this.myMetadataViewWidgetKeys.push(slotKey);
      return widget;
    }
  }

  // region 删除子视图块

  // 删除子视图块
  @Widget.Method()
  public async onDleteSubviewBlock(index: number) {
    if (index > -1) {
      const deleteWidget = this.myMetadataViewWidget.splice(index, 1);
      const deleteKey = this.myMetadataViewWidgetKeys.splice(index, 1);
      this.myMetadataViewWidgetLength--;
    }
  }

  // region 初始化子视图

  // mountedProcess 时,数据已经拿到,根据数据创建子视图
  protected async mountedProcess() {
    const resView = await queryViewDslByModelAndName(this.subviewModel, this.subviewName);
    if (this.value?.length) {
      this.value?.forEach(async (eachValue) => {
        this.createDynamicSubviewWidget(resView, eachValue);
      });
    } else if (!this.myMetadataViewWidgetLength) {
      // 默认给一个子视图
      this.createDynamicSubviewWidget(resView);
    }
  }

  // 监听子视图数据变化,并更新父视图数据
  public mounted(): void {
    super.mounted();
    this.mountedCallChaining?.callAfter(() => {
      // 监听子视图数据变化
      watch(
        () => this.myMetadataViewWidget.map((widget) => widget.getFormData()),
        async (newValues) => {
          this.reloadActiveRecords({
            ...this.formData,
            [this.itemName]: newValues
          });
        },
        { deep: true }
      );
    });
  }

  // 字段提交数据方法,提交所有子视图的数据
  public async submit() {
    const promises = this.myMetadataViewWidget.map(async (widget) => widget.getSubmitData());
    const submitValues = await Promise.all(promises);
    return {
      [this.itemName]: submitValues.map((submitValue) => submitValue?.records || {})
    };
  }
}

AddSubform.vue 动态添加表单 vue 组件

<template>
  <div class="AddSubviewBlockField">
    <div class="subview-block" v-for="(key, index) in myMetadataViewWidgetKeys" :key="key">
      <oio-icon @click="onDleteSubviewBlock(index)" icon="oinone-shanchu3" size="24" />
      <slot :name="key" />
    </div>
    <div class="add-icon">
      <a-divider />
      <oio-icon @click="onAddSubviewBlock" icon="oinone-add-circle" size="24" />
    </div>
  </div>
</template>

<script lang="ts" setup>
import { OioIcon } from '@oinone/kunlun-dependencies';
import { PropType } from 'vue';

const props = defineProps({
  myMetadataViewWidgetLength: {
    type: Number
  },
  myMetadataViewWidgetKeys: {
    type: Array as PropType<string[]>,
    default: () => []
  },
  onAddSubviewBlock: {
    type: Function as PropType<() => void>,
    default: () => {}
  },
  onDleteSubviewBlock: {
    type: Function as PropType<(index: number) => void>,
    default: () => {}
  },
});
</script>
<style lang="scss" scoped>
.AddSubviewBlockField {
  display: flex;
  flex-direction: column;
  gap: 48px;

  > .add-icon {
    display: flex;
    flex-direction: column;

    > .ant-divider-horizontal {
      margin: 0;
    }

    > .oio-icon {
      cursor: pointer;
      align-self: flex-end;
    }
  }

  > .subview-block {
    width: 100%;

    > .oio-icon {
      cursor: pointer;
    }
  }
}
</style>

MyMetadataViewWidget 数据隔离组件

import {
  ActiveRecord,
  ActiveRecords,
  CallChaining,
  FormWidget,
  MetadataViewWidget,
  queryDslWidget,
  Widget
} from '@oinone/kunlun-dependencies';

/**
 * 通过视图 handle 查找表单组件
 * @param viewHandle
 */
const queryFormWidgetByViewHandle = (viewHandle: string): FormWidget | null => {
  const baseViewWidget = Widget.select(viewHandle);
  const formWidget = queryDslWidget(baseViewWidget?.getChildrenInstance(), FormWidget);
  if (formWidget) {
    return formWidget as unknown as FormWidget;
  }
  return null;
};

export class MyMetadataViewWidget extends MetadataViewWidget {
  @Widget.Provide()
  public mountedCallChaining: CallChaining | undefined;

  @Widget.Provide()
  @Widget.Reactive()
  public dataSource: ActiveRecord[] = [];

  @Widget.Method()
  @Widget.Provide()
  public reloadDataSource(records: ActiveRecords | undefined) {
    if (Array.isArray(records)) {
      this.dataSource = records;
    } else {
      this.dataSource = [records || {}];
    }
  }

  @Widget.Provide()
  @Widget.Reactive()
  public activeRecords: ActiveRecord[] = [];

  @Widget.Method()
  @Widget.Provide()
  public reloadActiveRecords(records: ActiveRecords | undefined) {
    if (Array.isArray(records)) {
      this.activeRecords = records;
    } else {
      this.activeRecords = [records || {}];
    }
  }

  @Widget.Reactive()
  @Widget.Provide()
  public rootData: ActiveRecord[] | undefined;

  @Widget.Method()
  @Widget.Provide()
  public reloadRootData(records: ActiveRecords | undefined) {
    if (Array.isArray(records)) {
      this.rootData = records;
    } else {
      this.rootData = [records || {}];
    }
  }

  public initialize(props): this {
    this.mountedCallChaining = props.mountedCallChaining;
    this.subIndex = props.subIndex;
    super.initialize(props);
    return this;
  }

  protected mounted() {
    this.mountedCallChaining?.syncCall();
  }

  protected async validator() {
    const formWidget = queryFormWidgetByViewHandle(this.currentHandle);
    const res = await formWidget?.validator();
    return res;
  }

  public getFormData() {
    return this.activeRecords?.[0];
  }

  public async getSubmitData() {
    const formWidget = queryFormWidgetByViewHandle(this.currentHandle);
    return await formWidget?.submit();
  }

  protected getModelFields() {
    const formWidget = queryFormWidgetByViewHandle(this.currentHandle);
    return formWidget?.rootRuntimeContext.getRequestModelFields();
  }

  public setData(data: Record<string, unknown>) {
    if (data) {
      this.reloadDataSource(data);
      this.reloadActiveRecords(data);
      this.reloadRootData(data);
    }
  }

  /**
   * 当前子路径索引
   */
  @Widget.Reactive()
  protected subIndex: string | number | undefined;

  /**
   * 上级路径
   */
  @Widget.Reactive()
  @Widget.Inject('path')
  protected parentPath: string | undefined;

  /**
   * 完整路径
   */
  @Widget.Reactive()
  @Widget.Provide()
  public get path() {
    const { parentPath, subIndex } = this;
    let path = parentPath || '';
    return `${path}.metadata[${subIndex || ''}]`;
  }
}

原理分析

vue 插槽渲染

我们先从大家都熟悉的 vue 文件入手

<template>
  <div class="AddSubviewBlockField">
    <div class="subview-block" v-for="(key, index) in myMetadataViewWidgetKeys" :key="key">
      <oio-icon @click="onDleteSubviewBlock(index)" icon="oinone-shanchu3" size="24" />
      <slot :name="key" />
    </div>
    <div class="add-icon">
      <a-divider />
      <oio-icon @click="onAddSubviewBlock" icon="oinone-add-circle" size="24" />
    </div>
  </div>
</template>

可以看到模版的内容非常简单,上方一个循环,渲染子视图的列表,每个列表有个删除icon;下方一个 添加icon,点击触发添加子视图。
关键点是子视图循环渲染中,用到了插槽接收,也就意味着在 AddSubformWidget 中创建了插槽的内容,下面我们着重来看看这一部分的实现。

widget 创建插槽内容

核心代码,创建插槽

我们先来看看核心创建插槽的代码

      // 创建子视图组件
      const widget = this.createWidget(
        new MyMetadataViewWidget(runtimeContextHandle),
        slotKey, // 插槽名称
        {
          metadataHandle: runtimeContextHandle,
          rootHandle: runtimeContextHandle,
          mountedCallChaining: new CallChaining(),
          refreshCallChaining: new CallChaining(),
          inline: true,
          subIndex: this.myMetadataViewWidgetLength
        }
      );

核心就是 this.createWidget 方法,它会为当前 widget 创建一个子组件,即 MyMetadataViewWidget;第二个参数指定 vue 里接收的插槽名。

如果细心的你去看了 createWidget 的源码,你就会发现它只是:为子组件指定当前组件为父组件,初始化子组件,为当前组件添加子组件罢了。那么插槽是怎么创建出来的呢?
这就涉及到核心 buildWidgetComponent 方法,它会把当前 widget 渲染成 vue ,并把 this.setComponent(AddSubform) 设置的组件渲染出来。在渲染 AddSubform 时,会调用 this.resolveChildren() 为它创建插槽,前提是没有默认插槽(默认插槽是根据 DSL 渲染的,也就是说当前组件 DSL 里的子 widgets 必须为空)。

插槽内容

通过上述创建,插槽是有了,但插槽的内容是什么呢?
内容在 MyMetadataViewWidget 这个数据隔离的视图组件里,它继承了 MetadataViewWidget,会根据上下文视图最终dsl,渲染页面,以下是获取视图,为视图构建上下文的代码。

  // 按钮添加点击事件
  @Widget.Method()
  public async onAddSubviewBlock() {
    // 这里根据子视图模型,子视图名称,获取子视图
    const resView = await queryViewDslByModelAndName(this.subviewModel, this.subviewName);
    this.createDynamicSubviewWidget(resView);
  }

  // 创建子视图块
  public async createDynamicSubviewWidget(view, activeRecord: ActiveRecord = {}) {
    if (view) {
      // 根据子视图视图构建上下文
      const runtimeContext = createRuntimeContextByView(
        {
          type: ViewType.Form,
          model: view.model,
          modelName: view.modelDefinition.name,
          module: view.modelDefinition.module,
          moduleName: view.modelDefinition.moduleName,
          name: view.name,
          dsl: view.template
        },
        true,
        uniqueKeyGenerator(),
        this.currentHandle
      );
      // 取得上下文唯一标识
      const runtimeContextHandle = runtimeContext.handle;

      const slotKey = `Form_${uniqueKeyGenerator()}`;

      // 创建子视图组件
      const widget = this.createWidget(
        new MyMetadataViewWidget(runtimeContextHandle),
        slotKey, // 插槽名称
        {
          metadataHandle: runtimeContextHandle,
          rootHandle: runtimeContextHandle,
          mountedCallChaining: new CallChaining(),
          refreshCallChaining: new CallChaining(),
          inline: true,
          subIndex: this.myMetadataViewWidgetLength
        }
      );
      // 根据上下文,初始化最终 dsl
      widget.initContext(runtimeContext);
      // 初始化数据
      widget.setData(activeRecord);
      ......
      return widget;
    }
  }

Oinone社区 作者:银时原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/components/21426.html

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

(0)
银时的头像银时数式员工
上一篇 3天前
下一篇 1小时前

相关推荐

  • oio-empty-data 空数据状态

    何时使用 当目前没有数据时,用于显式的用户提示。 初始化场景时的引导创建流程。 API 参数 说明 类型 默认值 版本 description 自定义描述内容 string | v-slot – image 设置显示图片,为 string 时表示自定义图片地址 string | v-slot false imageStyle 图片样式 CSSProperties –

    2023年12月18日
    67900
  • oio-switch 开关

    API 参数 说明 类型 默认值 版本 autofocus 组件自动获取焦点 boolean false checked(v-model: checked ) 指定当前是否选中 checkedValue | unCheckedValue false checkedChildren 选中时的内容 slot checkedValue 选中时的值 boolean | string | number true disabled 是否禁用 boolean false loading 加载中的开关 boolean false unCheckedChildren 非选中时的内容 slot unCheckedValue 非选中时的值 boolean | string | number false 事件 事件名称 说明 回调参数 change 变化时回调函数 Function(checked: boolean | string | number, event: Event)

    2023年12月18日
    54800
  • oio-button 按钮

    主按钮:用于主行动点,一个操作区域只能有一个主按钮。 默认按钮:用于没有主次之分的一组行动点。 虚线按钮:常用于添加操作。 文本按钮:用于最次级的行动点。 链接按钮:一般用于链接,即导航至某位置。 以及四种状态属性与上面配合使用。 危险:删除/移动/修改权限等危险操作,一般需要二次确认。 禁用:行动点不可用的时候,一般需要文案解释。 加载中:用于异步操作等待反馈的时候,也可以避免多次提交。 API 按钮的属性说明如下: 属性 说明 类型 默认值 版本 block 将按钮宽度调整为其父宽度的选项 boolean false disabled 按钮失效状态 boolean false icon 设置按钮的图标类型 v-slot – loading 设置按钮载入状态 boolean | { delay: number } false type 设置按钮类型 primary | ghost | dashed | link | text | default default 事件 事件名称 说明 回调参数 版本 click 点击按钮时的回调 (event) => void 支持原生 button 的其他所有属性。

    2023年12月18日
    39400
  • 前端自定义组件之单页面步骤条

    本文将讲解如何通过自定义,实现单页面的步骤条组件。其中每个步骤的元素里都是界面设计器拖出来的。 实现路径 整体的实现思路是界面设计器拖个选项卡组件,自定义这个选项卡,里面的每个选项页都当成一步渲染出来,每一步的名称是选项页的标题。 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日
    24600
  • oio-grid 栅格

    24 栅格系统。 <oio-row :gutter="24"> <oio-col :span="12"></oio-col> <oio-col :span="12"></oio-col> </oio-row> 概述 布局的栅格化系统,我们是基于行(row)和列(col)来定义信息区块的外部框架,以保证页面的每个区域能够稳健地排布起来。下面简单介绍一下它的工作原理: 通过\row\在水平方向建立一组\column\(简写 col) 你的内容应当放置于\col\内,并且,只有\col\可以作为\row\的直接元素 栅格系统中的列是指 1 到 24 的值来表示其跨越的范围。例如,三个等宽的列可以使用 \<a-col :span="8" />\ 来创建 如果一个\row\中的\col\总和超过 24,那么多余的\col\会作为一个整体另起一行排列 Flex 布局 我们的栅格化系统支持 Flex 布局,允许子元素在父节点内的水平对齐方式 – 居左、居中、居右、等宽排列、分散排列。子元素与子元素之间,支持顶部对齐、垂直居中对齐、底部对齐的方式。同时,支持使用 order 来定义元素的排列顺序。 Flex 布局是基于 24 栅格来定义每一个『盒子』的宽度,但不拘泥于栅格。 API Row 成员 说明 类型 默认值 align flex 布局下的垂直对齐方式:top middle bottom string top gutter 栅格间隔,可以写成像素值或支持响应式的对象写法来设置水平间隔 { xs: 8, sm: 16, md: 24}。或者使用数组形式同时设置 [水平间距, 垂直间距](1.5.0 后支持)。 number/object/array 0 justify flex 布局下的水平排列方式:start end center space-around space-between string start wrap 是否自动换行 boolean false Col 成员 说明 类型 默认值 版本 flex flex 布局填充 string|number – offset 栅格左侧的间隔格数,间隔内不可以有栅格 number 0 order 栅格顺序,flex 布局模式下有效 number 0 pull 栅格向左移动格数 number 0 push 栅格向右移动格数 number 0 span 栅格占位格数,为 0 时相当于 display: none number – xxxl ≥2000px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – xs <576px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – sm ≥576px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – md ≥768px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – lg ≥992px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – xl ≥1200px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – xxl ≥1600px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object –

    2023年12月18日
    58600

Leave a Reply

登录后才能评论