自定义组件之手动渲染弹出层(v4)

阅读之前

你应该:

弹出层组件

我们内置了两个弹出层组件,弹窗(Dialog)抽屉(Drawer),以下所有内容全部围绕弹窗(Dialog)进行描述,抽屉相关内容与弹窗完全一致。

下面这个对照表格可以帮助你区分两个弹出层组件的异同:

弹出层相关功能 弹窗(Dialog) 抽屉(Drawer)
API方法 Dialog#create Drawer#create
内置组件 DialogWidget DrawerWidget
内置插槽 header, default, footer header, default, footer

渲染弹出层的方式

我们提供了三种渲染弹出层组件的方式,根据不同的情况使用不同的方式可以让实现变得更简单。

  • 使用Dialog#createAPI方法创建弹窗:一般用于简单场景,动作区无法进行自定义。
  • 使用DSL渲染能力创建弹窗
    • 调用ActionWidget#click方法打开弹窗(适用于自动渲染场景)
    • 使用createWidget创建DialogWidget并打开弹窗(适用于手动渲染场景)
  • 使用第三方组件创建弹窗:一般用于弹窗需要自定义的场景中,性能最优,可用于任何场景。

使用Dialog#createAPI方法创建弹窗

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

view.ts
export const template = `<view>
    <field data="id" invisible="true" />
    <field data="code" label="编码" />
    <field data="name" label="名称" />
</view>`;
ManualDemoWidget.ts
import {
  BaseElementWidget,
  createRuntimeContextForWidget,
  Dialog,
  FormWidget,
  MessageHub,
  RuntimeView,
  SPI,
  ViewCache,
  ViewType,
  Widget
} from '@kunlun/dependencies';
import ManualDemo from './ManualDemo.vue';
import { template } from './view';

@SPI.ClassFactory(BaseElementWidget.Token({ widget: 'ManualDemo' }))
export class ManualDemoWidget extends BaseElementWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(ManualDemo);
    return this;
  }

  @Widget.Method()
  public async openPopup() {
    // 获取运行时视图
    const view = await this.fetchViewByCompile();
    if (!view) {
      console.error('Invalid view');
      return;
    }

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

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

    // 创建弹窗
    const dialogWidget = Dialog.create();

    // 设置弹窗属性
    dialogWidget.setTitle('这是一个演示弹窗');

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

    dialogWidget.on('ok', () => {
      MessageHub.info('click ok');

      // 关闭弹窗
      dialogWidget.onVisibleChange(false);
    });

    dialogWidget.on('cancel', () => {
      MessageHub.info('click cancel');

      // 关闭弹窗
      dialogWidget.onVisibleChange(false);
    });

    // 打开弹窗
    dialogWidget.onVisibleChange(true);
  }

  public async fetchViewByCompile(): Promise<RuntimeView | undefined> {
    // 模型编码
    const model = ${model};

    // 通过编译获取视图(非完整视图数据)
    const view = await ViewCache.compile(model, '$$popup_demo', template);
    if (!view) {
      return;
    }
    // 补充视图类型
    view.type = ViewType.Form;

    return view;
  }
}
ManualDemo.vue
<template>
  <div class="manual-demo">
    <oio-button @click="openPopup">打开弹窗</oio-button>
  </div>
</template>
<script lang="ts">
import { OioButton } from '@kunlun/vue-ui-antd';
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'ManualDemo',
  components: {
    OioButton
  },
  props: {
    openPopup: {
      type: Function
    }
  }
});
</script>
视图DSL
<view type="FORM">
    <template slot="form">
        <field data="id" invisible="true" />
        <field data="code" />
        <field data="name" />
        <element widget="ManualDemo" />
    </template>
</view>

关键点详解

......

使用DSL渲染能力创建弹窗(调用ActionWidget#click方法)

以下是一个自定义组件的完整示例,视图被定义在DSL模板中。

ManualDemoWidget.ts
import { ActionWidget, BaseElementWidget, DslDefinitionWidget, SPI, Widget } from '@kunlun/dependencies';
import ManualDemo from './ManualDemo.vue';

@SPI.ClassFactory(BaseElementWidget.Token({ widget: 'ManualDemo' }))
export class ManualDemoWidget extends BaseElementWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(ManualDemo);
    return this;
  }

  @Widget.Method()
  public async openPopup() {
    const actionWidget = this.getChildren().find((v) => {
      if (v instanceof DslDefinitionWidget) {
        const slotName = v.getSlotName();
        if (slotName === 'popupAction' && v instanceof ActionWidget) {
          return v;
        }
      }
      return false;
    }) as ActionWidget;

    if (actionWidget) {
      await actionWidget.click();
      actionWidget.forceUpdate();
    }
  }
}
ManualDemo.vue
<template>
  <div class="manual-demo">
    <oio-button @click="openPopup">打开弹窗</oio-button>
    <div style="display: none">
      <slot name="popupAction" />
    </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: {
    openPopup: {
      type: Function
    }
  }
});
</script>
视图DSL
<view type="FORM">
    <template slot="form">
        <field data="id" invisible="true" />
        <field data="code" />
        <field data="name" />
        <element widget="ManualDemo">
            <template slot="popupAction">
                <action name="$$popup_action_demo" actionType="VIEW" contextType="CONTEXT_FREE" target="DIALOG" resModel="${model}">
                    <template slot="default" title="这是一个演示弹窗">
                        <view type="FORM">
                            <template slot="actionBar">
                                <action name="$$internal_DialogCancel" type="default" />
                                <action name="create" />
                            </template>
                            <template slot="form">
                                <field data="id" invisible="true" />
                                <field data="code" />
                                <field data="name" />
                            </template>
                        </view>
                    </template>
                </action>
            </template>
        </element>
    </template>
</view>

关键点详解

......

使用DSL渲染能力创建弹窗(创建DialogWidget组件)

以下是一个自定义组件的完整示例,其使用ViewCache#compule方法获取视图,该视图对应的是弹窗组件的渲染模板。

view.ts
export const template = `<view title="这是一个演示弹窗">
    <template slot="default">
        <view type="FORM">
            <element widget="form">
                <field data="id" invisible="true" />
                <field data="code" />
                <field data="name" />
            </element>
        </view>
    </template>
    <template slot="footer">
        <element widget="actionBar">
            <action name="$$internal_DialogCancel" type="default" />
            <action name="create" />
        </element>
    </template>
</view>`;
ManualDemoWidget.ts
import {
  BaseElementWidget,
  createRuntimeContextForWidget,
  DialogWidget,
  RuntimeView,
  SPI,
  ViewCache,
  ViewType,
  Widget
} from '@kunlun/dependencies';
import ManualDemo from './ManualDemo.vue';
import { template } from './view';

@SPI.ClassFactory(BaseElementWidget.Token({ widget: 'ManualDemo' }))
export class ManualDemoWidget extends BaseElementWidget {
  public initialize(props) {
    super.initialize(props);
    this.setComponent(ManualDemo);
    return this;
  }

  @Widget.Method()
  public async openPopup() {
    // 获取运行时视图
    const view = await this.fetchViewByCompile();
    if (!view) {
      console.error('Invalid view');
      return;
    }

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

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

    const dialogWidget = this.createWidget(new DialogWidget(), 'popupWidget', {
      metadataHandle: runtimeContextHandle,
      rootHandle: runtimeContextHandle,
      dataSource: formData,
      activeRecords: formData,
      template: runtimeContext.viewTemplate,
      inline: true,
      mountedVisible: true
    });

    dialogWidget.on('ok', async () => {
      dialogWidget.onVisibleChange(false);
    });
    dialogWidget.on('cancel', () => {
      dialogWidget.onVisibleChange(false);
    });

    this.forceUpdate();
  }

  public async fetchViewByCompile(): Promise<RuntimeView | undefined> {
    // 模型编码
    const model = ${model};

    // 通过编译获取视图(非完整视图数据)
    const view = await ViewCache.compile(model, '$$popup_demo', template);
    if (!view) {
      return;
    }
    // 补充视图类型
    view.type = ViewType.Form;

    return view;
  }
}
ManualDemo.vue
<template>
  <div class="manual-demo">
    <oio-button @click="openPopup">打开弹窗</oio-button>
    <div style="display: none">
      <slot name="popupWidget" />
    </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: {
    openPopup: {
      type: Function
    }
  }
});
</script>
视图DSL
<view type="FORM">
    <template slot="form">
        <field data="id" invisible="true" />
        <field data="code" />
        <field data="name" />
        <element widget="ManualDemo" />
    </template>
</view>

关键点详解

......

使用第三方组件创建弹窗

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

view.ts
export const template = `<view>
    <field data="id" invisible="true" />
    <field data="code" label="编码" />
    <field data="name" label="名称" />
</view>`;
ManualDemoWidget.ts
import {
  BaseElementWidget,
  createRuntimeContextForWidget,
  FormWidget,
  MessageHub,
  RuntimeView,
  SPI,
  ViewCache,
  ViewType,
  Widget
} from '@kunlun/dependencies';
import ManualDemo from './ManualDemo.vue';
import { template } 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 openPopup(): Promise<void> {
    // 获取运行时视图
    const view = await this.fetchViewByCompile();
    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.formWidget = this.createWidget(FormWidget, 'formWidget', {
      metadataHandle: runtimeContextHandle,
      rootHandle: runtimeContextHandle,
      dataSource: formData,
      activeRecords: formData,
      template: runtimeContext.viewTemplate,
      inline: true
    });
    this.forceUpdate();
  }

  @Widget.Method()
  public onEnter() {
    MessageHub.info('click ok');
    return true;
  }

  @Widget.Method()
  public onCancel() {
    MessageHub.info('click cancel');
    return true;
  }

  public async fetchViewByCompile(): Promise<RuntimeView | undefined> {
    // 模型编码
    const model = ${model};

    // 通过编译获取视图(非完整视图数据)
    const view = await ViewCache.compile(model, '$$popup_demo', template);
    if (!view) {
      return;
    }
    // 补充视图类型
    view.type = ViewType.Form;

    return view;
  }
}
ManualDemo.vue
<template>
  <div class="manual-demo">
    <oio-button @click="openModal">打开弹窗</oio-button>
    <oio-modal v-model:visible="visible" title="这是一个演示弹窗" :enter-callback="onEnter" :cancel-callback="onCancel">
      <slot name="formWidget" />
    </oio-modal>
  </div>
</template>
<script lang="ts">
import { ManualWidget } from '@kunlun/dependencies';
import { OioButton, OioModal } from '@kunlun/vue-ui-antd';
import { defineComponent, nextTick, ref } from 'vue';

export default defineComponent({
  name: 'ManualDemo',
  mixins: [ManualWidget],
  components: {
    OioButton,
    OioModal
  },
  props: {
    openPopup: {
      type: Function
    },
    onEnter: {
      type: Function
    },
    onCancel: {
      type: Function
    }
  },
  setup(props) {
    const visible = ref(false);

    const openModal = async () => {
      await props.openPopup?.();
      nextTick(() => {
        visible.value = true;
      });
    };

    return {
      visible,

      openModal
    };
  }
});
</script>
视图DSL
<view type="FORM">
    <template slot="form">
        <field data="id" invisible="true" />
        <field data="code" />
        <field data="name" />
        <element widget="ManualDemo" />
    </template>
</view>

关键点详解

......

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

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

(0)
nation的头像nation数式员工
上一篇 2023年6月20日 pm4:07
下一篇 2023年11月2日 pm1:58

相关推荐

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

    引言 本文将通过前端的开发者模式带领大家提高自定义组件的开发效率 支持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.8K00
  • 自定义组件之手动渲染任意视图(v4)

    private metadataViewWidget: MetadataViewWidget | null | undefined; private async renderCustomView(model: string, viewName: string, slotName?: string) { const view = await ViewCache.get(model, viewName); if (!view) { return; } if (this.metadataViewWidget) { this.metadataViewWidget.dispose(); this.metadataViewWidget = null; } const metadataViewWidget = this.createWidget(MetadataViewWidget, slotName, { metadataHandle: this.metadataHandle, rootHandle: this.rootHandle, internal: true, inline: true, automatic: true }); this.metadataViewWidget = metadataViewWidget; metadataViewWidget.initContextByView(view); this.forceUpdate(); }

    2025年3月6日
    1.1K00
  • 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日
    69000
  • 创建与编辑一体化

    在业务操作中,用户通常期望能够在创建页面后立即进行编辑,以减少频繁切换页面的步骤。我们可以充分利用Oinone平台提供的创建与编辑一体化功能,使操作更加高效便捷。 通过拖拽实现表单页面设计 在界面设计器中,我们首先需要设计出对应的页面。完成页面设计后,将需要的动作拖入设计好的页面。这个动作的关键在于支持一个功能,即根据前端传入的数据是否包含id来判断是创建操作还是编辑操作。 动作的属性配置如下: 前端自定义动作 一旦页面配置完成,前端需要对这个动作进行自定义。以下是一个示例的代码: @SPI.ClassFactory( ActionWidget.Token({ actionType: [ActionType.Server], model: '模型', name: '动作的名称' }) ) export class CreateOrUpdateServerActionWidget extends ServerActionWidget { @Widget.Reactive() protected get updateData(): boolean { return true; } } 通过以上步骤,我们实现了一个更智能的操作流程,使用户能够在创建页面的同时进行即时的编辑,从而提高了整体操作效率。这种创建与编辑一体化的功能不仅使操作更加顺畅,同时也为用户提供了更灵活的工作流程。

    2023年11月21日
    1.8K00
  • 如何自定义指定页面的样式

    可以通过在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.2K00

Leave a Reply

登录后才能评论