前端页面嵌套

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

代码实现

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-dropdown 下拉菜单

    向下弹出的列表。 何时使用 当页面上的操作命令过多时,用此组件可以收纳操作元素。点击或移入触点,会出现一个下拉菜单。可在列表中进行选择,并执行相应的命令。 用于收罗一组命令操作。 Select 用于选择,而 Dropdown 是命令集合。 API 属性如下 参数 说明 类型 默认值 destroyOnHide 关闭后是否销毁 Dropdown boolean false disabled 菜单是否禁用 boolean – getTriggerContainer 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。 Function(triggerNode) () => document.body overlay(v-slot) 菜单 Menu – overlayClassName 下拉根元素的类名称 string – overlayStyle 下拉根元素的样式 object – placement 菜单弹出位置 bottomLeft | bottom | bottomRight | topLeft | top | topRight bottomLeft trigger 触发下拉的行为, 移动端不支持 hover Array<click|hover|contextmenu> ['hover'] visible(v-model:visible) 菜单是否显示 boolean – 事件 事件名称 说明 回调参数 onUpdateValue 菜单显示状态改变时调用,参数为 visible。点击菜单按钮导致的消失不会触发 function(visible)

    2023年12月18日
    64300
  • oio-modal 对话框

    API 参数 说明 类型 默认值 版本 cancelText 取消按钮文字 string| slot 取消 closable 是否显示右上角的关闭按钮 boolean true closeIcon 自定义关闭图标 VNode | slot – confirmLoading 确定按钮 loading boolean 无 destroyOnClose 关闭时销毁 Modal 里的子元素 boolean false footer 底部内容,当不需要默认底部按钮时,可以设为 :footerInvisible="true" slot 确定取消按钮 getTriggerContainer 指定 Modal 挂载的 HTML 节点 (instance): HTMLElement () => document.body keyboard 是否支持键盘 esc 关闭 boolean true mask 是否展示遮罩 boolean true maskClosable 点击蒙层是否允许关闭 boolean true enterText 确认按钮文字 string 确定 title 标题 string|slot 无 visible(v-model:visible) 对话框是否可见 boolean 无 width 宽度 string|number 520 wrapClassName 对话框外层容器的类名 string – zIndex 设置 Modal 的 z-index number 1000 cancelCallback 点击遮罩层或右上角叉或取消按钮的回调, return true则关闭弹窗 function(e) enterCallback 点击确定回调 function(e)

    2023年12月18日
    59300
  • 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日
    57800
  • OioNotification 通知提醒框

    全局展示通知提醒信息。 何时使用 在系统四个角显示通知提醒信息。经常用于以下情况: 较为复杂的通知内容。 带有交互的通知,给出用户下一步的行动点。 系统主动推送。 API OioNotification.success(title,message, config) OioNotification.error(title,message, config) OioNotification.info(title,message, config) OioNotification.warning(title,message, config) config 参数如下: 参数 说明 类型 默认值 版本 duration 默认 3 秒后自动关闭 number 3 class 自定义 CSS class string –

    2023年12月18日
    59700
  • oio-checkbox 对选框

    API 属性 Checkbox 参数 说明 类型 默认值 版本 autofocus 自动获取焦点 boolean false checked(v-model:checked) 指定当前是否选中 boolean false disabled 失效状态 boolean false indeterminate 设置 indeterminate 状态,只负责样式控制 boolean false value 与 CheckboxGroup 组合使用时的值 boolean | string | number – 事件 事件名称 说明 回调参数 版本 change 变化时回调函数 Function(e:Event) –

    2023年12月18日
    65500

Leave a Reply

登录后才能评论