前端页面嵌套

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

代码实现

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)
银时的头像银时数式员工
上一篇 2025年7月18日 pm3:42
下一篇 2025年7月21日 pm4:02

相关推荐

  • oio-input 输入框

    代码演示 <oio-input v-model:value="value"></oio-input> API Input 参数 说明 类型 默认值 版本 addonAfter 带标签的 input,设置后置标签 string|slot addonBefore 带标签的 input,设置前置标签 string|slot allowClear 可以点击清除图标删除内容 boolean defaultValue 输入框默认内容 string disabled 是否禁用状态,默认为 false boolean false maxlength 最大长度 number prefix 带有前缀图标的 input slot showCount 是否展示字数 boolean false suffix 带有后缀图标的 input slot type 声明 input 类型,同原生 input 标签的 type 属性,见:MDN(请直接使用 <a-textarea /> 代替 type="textarea")。 string text value(v-model:value) 输入框内容 string Input 事件 事件名称 说明 回调参数 update:value 输入框内容变化时的回调 function(e) pressEnter 按下回车的回调 function(e) Input.Search 代码演示 <oio-input-search v-model:value="value"></oio-input-search> Input.Search 事件 事件名称 说明 回调参数 search 点击搜索或按下回车键时的回调 function(value, event) 其余属性和 Input 一致。

    2023年12月18日
    2.0K00
  • oio-cascader 级联选择

    级联选择框。 何时使用 需要从一组相关联的数据集合进行选择,例如省市区,公司层级,事物分类等。 从一个较大的数据集合中进行选择时,用多级分类进行分隔,方便选择。 比起 Select 组件,可以在同一个浮层中完成选择,有较好的体验。 API <oio-cascader :options="options" v-model:value="value" /> 参数 说明 类型 默认值 Version allowClear 是否支持清除 boolean true autofocus 自动获取焦点 boolean false changeOnSelect (单选时生效)当此项为 true 时,点选每级菜单选项值都会发生变化,具体见上面的演示 boolean false disabled 禁用 boolean false displayRender 选择后展示的渲染函数,可使用 #displayRender="{labels, selectedOptions}" ({labels, selectedOptions}) => VNode labels => labels.join(' / ') dropdownClassName 自定义浮层类名 string – getTriggerContainer 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。 Function(triggerNode) () => document.body loadData 用于动态加载选项,无法与 showSearch 一起使用 (selectedOptions) => void – maxTagCount 最多显示多少个 tag,响应式模式会对性能产生损耗 number | responsive – maxTagPlaceholder 隐藏 tag 时显示的内容 v-slot | function(omittedValues) – multiple 支持多选节点 boolean – options 可选项数据源 – placeholder 输入框占位文本 string ‘请选择’ searchValue 设置搜索的值,需要与 showSearch 配合使用 string – showSearch 在选择框中显示搜索框 boolean false tagRender 自定义 tag 内容,多选时生效 slot – value(v-model:value) 指定选中项 string[] | number[] – showSearch showSearch 为对象时,其中的字段: 参数 说明 类型 默认值 filterOption 接收 inputValue path 两个参数,当 path 符合筛选条件时,应返回 true,反之则返回 false。 function(inputValue, path): boolean 事件 事件名称 说明 回调参数 版本 change 选择完成后的回调 (value, selectedOptions) => void – search 监听搜索,返回输入的值 (value) => void – Option interface Option { value: string | number; label?: any; disabled?: boolean; children?: Option[]; // 标记是否为叶子节点,设置了 `loadData` 时有效 // 设为 `false` 时会强制标记为父节点,即使当前节点没有 children,也会显示展开图标 isLeaf?: boolean; }

    2023年12月18日
    95000
  • 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日
    89400
  • 前端表格复制

    我们可能会遇到表格复制的需求,也就是表格填写的时候,不是增加一行数据,而是增加一个表格。以下是代码实现和原理分析。 代码实现 在 boot 工程的 main.ts 中加入以下代码 import { registerDesignerFieldWidgetCreator, selectorDesignerFieldWidgetCreator } from '@oinone/kunlun-ui-designer-dependencies'; // 注册无代码组件,将表头分组的无代码组件,注册成字段表格组件 registerDesignerFieldWidgetCreator( { widget: 'DynamicCreateTable' }, selectorDesignerFieldWidgetCreator({ widget: TABLE_WIDGET })! ); DynamicCreateTableWidget 动态添加表格 ts 组件 import { FormO2MTableFieldWidget, Widget, DslDefinition, RuntimeView, SubmitValue, BaseFieldWidget, ModelFieldType, SPI, ViewType, ActiveRecord, uniqueKeyGenerator } from '@oinone/kunlun-dependencies'; import { MyMetadataViewWidget } from './MyMetadataViewWidget'; import DynamicCreateTable from './DynamicCreateTable.vue'; @SPI.ClassFactory( BaseFieldWidget.Token({ viewType: ViewType.Form, ttype: ModelFieldType.OneToMany, widget: 'DynamicCreateTable' }) ) export class DynamicCreateTableWidget extends FormO2MTableFieldWidget { public myMetadataViewWidget: MyMetadataViewWidget[] = []; @Widget.Reactive() public myMetadataViewWidgetLength = 0; @Widget.Reactive() public myMetadataViewWidgetKeys: string[] = []; protected props: Record<string, unknown> = {}; public initialize(props) { super.initialize(props); this.props = props; this.setComponent(DynamicCreateTable); return this; } // region 创建动态表格 @Widget.Method() public async createTableWidget(record: ActiveRecord) { const index = this.myMetadataViewWidget.length; const handle = uniqueKeyGenerator(); const slotKey = `MyMetadataViewWidget_${handle}`; const widget = this.createWidget( new MyMetadataViewWidget(handle), slotKey, // 插槽名称 { subIndex: index, metadataHandle: this.metadataHandle, rootHandle: this.rootHandle, automatic: true, internal: true, inline: true } ); this.initDynamicSubview(this.props, widget); widget.setData(record); this.myMetadataViewWidgetLength++; this.myMetadataViewWidgetKeys.push(slotKey); this.myMetadataViewWidget.push(widget); } protected initDynamicSubview(props: Record<string, unknown>, widget: MyMetadataViewWidget) { const { currentViewDsl } = this; let viewDsl = currentViewDsl; if (!viewDsl) { viewDsl = this.getViewDsl(props)…

    2025年7月21日
    59400
  • oio-select 选择器

    API Select props 参数 说明 类型 默认值 版本 allowClear 支持清除 boolean false autofocus 默认获取焦点 boolean false clearIcon 自定义的多选框清空图标 VNode | slot – disabled 是否禁用 boolean false dropdownClassName 下拉菜单的 className 属性 string – dropdownRender 自定义下拉框内容 ({menuNode: VNode, props}) => VNode | v-slot – filterOption 是否根据输入项进行筛选。当其为一个函数时,会接收 inputValue option 两个参数,当 option 符合筛选条件时,应返回 true,反之则返回 false。 boolean | function(inputValue, option) true getTriggerContainer 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。 function(triggerNode) () => document.body menuItemSelectedIcon 自定义当前选中的条目图标 VNode | slot – options options 数据,如果设置则不需要手动构造 selectOption 节点 array<{value, label, [disabled, key, title]}> [] placeholder 选择框默认文字 string|slot – removeIcon 自定义的多选框清除图标 VNode | slot – suffixIcon 自定义的选择框后缀图标 VNode | slot – value(v-model:value) 指定当前选中的条目 string|string[]|number|number[] – 注意,如果发现下拉菜单跟随页面滚动,或者需要在其他弹层中触发 Select,请尝试使用 getPopupContainer={triggerNode => triggerNode.parentNode} 将下拉弹层渲染节点固定在触发器的父元素中。 事件 事件名称 说明 回调参数 blur 失去焦点的时回调 function change 选中 option,或 input 的 value 变化(combobox 模式下)时,调用此函数 function(value, option:Option/Array<Option>) deselect 取消选中时调用,参数为选中项的 value (或 key) 值,仅在 multiple 或 tags 模式下生效 function(value,option:Option) dropdownVisibleChange 展开下拉菜单的回调 function(open) focus 获得焦点时回调 function inputKeyDown 键盘按下时回调 function mouseenter 鼠标移入时回调 function mouseleave 鼠标移出时回调 function popupScroll 下拉列表滚动时的回调 function search 文本框值变化时回调 function(value: string) select 被选中时调用,参数为选中项的 value (或 key) 值 function(value, option:Option)

    2023年12月18日
    68300

Leave a Reply

登录后才能评论