自定义视图部分区域渲染设计器的配置

自定义视图与界面设计器配置对接

在日常开发中,我们经常会遇到自定义视图的需求。自定义视图不仅需要与平台机制结合,还要实现与界面设计器中配置的字段和动作的无缝对接。本文将介绍如何将自定义视图与界面设计器中配置的字段和动作的无缝对接,实现字段和动作的渲染。

用大白话来讲就是:当前页面一部分是自定义的,一部分是设计器生成的

代码地址

目录

  1. 自定义表单视图与字段、动作的结合
  2. 自定义表格视图与字段、动作的结合

自定义表单视图与字段、动作的结合

首先需要在界面设计器配置好对应界面,虽然配置的页面样式跟期望展示的 UI 的不一样,但是数据的分发、汇总以及动作的交互也是一致的,所以我们可以通过自定义的方式替换这个页面的 UI,但是数据以及动作,是完全可以通过平台的能力获取的。

注册表单对应的 SPI

// CustomFormWidget.ts

import CustomForm from './CustomForm.vue';

@SPI.ClassFactory(
  BaseElementWidget.Token({
    viewType: ViewType.Form,
    widget: 'CustomFormWidget'
  })
)
export class CustomFormWidget extends FormWidget {
  public initialize(props: BaseElementObjectViewWidgetProps): this {
    super.initialize(props);
    this.setComponent(CustomForm);
    return this;
  }
}
<!-- CustomForm.vue -->

<template>
  <div class="custom-form-container">
    <div class="custom-form-tip">自定义视图</div>
  </div>
</template>
<script lang="ts">
  import { DslRender, DslRenderDefinition, PropRecordHelper } from '@kunlun/dependencies';
  import { createVNode, defineComponent, onMounted, PropType, ref, VNode } from 'vue';

  export default defineComponent({
    inheritAttrs: false,
    props: {
      formData: {
        type: Object as PropType<Record<string, any>>,
        default: () => ({})
      }
    }
  });
</script>

在上述的代码中,我们继承的是 FormWidget,那么这个页面会自动发起对应的请求,所有的数据都在 formData 中。

表单视图渲染动作

在最开始我们讲到过,当前页面是在界面设计器配置过,所有在CustomFormWidget里面是可以拿到当前页面配置的元数据信息,所以我们可以拿到界面设计器配置的字段跟动作

/// CustomFormWidget.ts

@Widget.Method()
protected renderActionVNodes() {
  // 从dsl中获取actionBar,actionBar里面包含了界面设计器配置的动作
  const actionBar = this.metadataRuntimeContext.viewDsl?.widgets.find((w) => w.slot === 'actionBar');

  if (actionBar) {
    // actionBar.widgets 就是界面设计器配置的动作,借助平台提供的DslRender.render方法,将对应的dsl转换成VNode
    return actionBar.widgets.map((w, index) =>
      DslRender.render({
        ...w,
        __index: index
      })
    );
  }

  return null;
}

因为 renderActionVNodes 方法返回的是 VNode,所以我们必须借助 vue 的 render 函数才能渲染。

<!-- ActionRender.vue -->

<script lang="ts">
  import { defineComponent } from 'vue';

  export default defineComponent({
    inheritAttrs: false,
    props: {
      renderActionVNodes: {
        type: Function,
        required: true
      }
    },
    render() {
      return this.renderActionVNodes();
    }
  });
</script>

ActionRender.vue中,通过 props 接收renderActionVNodes 方法,最终在 render 函数中执行,所以只需要在CustomForm.vue导入ActionRender.vue

<template>
  <div class="custom-form-container">
    <div class="custom-form-tip">自定义视图</div>

    <p style="color: red">下面是通过平台机制生成的动作</p>

    <div style="display: flex; gap: 10px">
      <action-render :renderActionVNodes="renderActionVNodes"></action-render>
    </div>
  </div>
</template>
<script lang="ts">
  import { DslRender, DslRenderDefinition, PropRecordHelper } from '@kunlun/dependencies';
  import { createVNode, defineComponent, onMounted, PropType, ref, VNode } from 'vue';
  import ActionRender from './components/ActionRender.vue';

  export default defineComponent({
    inheritAttrs: false,
    props: {
      renderActionVNodes: {
        type: Function,
        required: true
      }
    },
    components: {
      ActionRender
    }
  });
</script>
<style lang="scss">
  .custom-form-container {
    .custom-form-tip {
      color: var(--oio-primary-color);
      margin-bottom: var(--oio-margin);
      font-size: 24px;
    }
  }
</style>

表单视图渲染字段

渲染界面设计器配置的字段和刚刚讲到的动作是一致的。

// CustomFormWidget.ts
@Widget.Method()
  protected renderFieldVNodes() {
    // 获取界面设计器配置的字段
    const modelFields = this.metadataRuntimeContext.model.modelFields;

    if (modelFields.length) {
      //借助平台提供的DslRender.render方法,将对应的dsl转换成VNode
      return modelFields.map((w) => DslRender.render(w.template!));
    }

    return null;
  }

因为renderFieldVNodes 返回的也是 VNode,所以我们还是需要借助 vue 的 render 函数来渲染.

<!-- FieldRender.vue -->
<script lang="ts">
  import { defineComponent } from 'vue';

  export default defineComponent({
    inheritAttrs: false,
    props: {
      renderFieldVNodes: {
        type: Function,
        required: true
      }
    },
    render() {
      return this.renderFieldVNodes();
    }
  });
</script>

最终也是在CustomForm.vue中导入FieldRender.vue

下面是完整的代码:

// CustomFormWidget.ts

import {
  BaseElementObjectViewWidgetProps,
  BaseElementWidget,
  DslRender,
  FORM_WIDGET,
  FormWidget,
  SPI,
  ViewType,
  Widget
} from '@kunlun/dependencies';

import CustomForm from './CustomForm.vue';

@SPI.ClassFactory(
  BaseElementWidget.Token({
    viewType: ViewType.Form,
    widget: 'CustomFormWidget'
  })
)
export class CustomFormWidget extends FormWidget {
  public initialize(props: BaseElementObjectViewWidgetProps): this {
    super.initialize(props);
    this.setComponent(CustomForm);
    return this;
  }

  /**
   * 渲染字段VNode
   */
  @Widget.Method()
  protected renderFieldVNodes() {
    const modelFields = this.metadataRuntimeContext.model.modelFields;

    if (modelFields.length) {
      return modelFields.map((w) => DslRender.render(w.template!));
    }

    return null;
  }

  /**
   * 渲染动作VNode
   */
  @Widget.Method()
  protected renderActionVNodes() {
    const actionBar = this.metadataRuntimeContext.viewDsl?.widgets.find((w) => w.slot === 'actionBar');

    if (actionBar) {
      return actionBar.widgets.map((w, index) =>
        DslRender.render({
          ...w,
          __index: index
        })
      );
    }

    return null;
  }

  @Widget.Reactive()
  protected showFieldVNode = false;

  protected async mountedProcess() {
    await super.mountedProcess();

    this.showFieldVNode = true;
  }
}
<!-- CustomForm.vue -->
<template>
  <div class="custom-form-container">
    <div class="custom-form-tip">自定义视图</div>

    <p style="color: red">下面是通过平台机制生成的字段</p>

    <field-render v-if="showFieldVNode" :renderFieldVNodes="renderFieldVNodes"></field-render>

    <p style="color: red">下面是通过平台机制生成的动作</p>

    <div style="display: flex; gap: 10px"><action-render :renderActionVNodes="renderActionVNodes"></action-render></div>
  </div>
</template>
<script lang="ts">
  import { DslRender, DslRenderDefinition, PropRecordHelper } from '@kunlun/dependencies';
  import { createVNode, defineComponent, onMounted, PropType, ref, VNode } from 'vue';
  import FieldRender from './components/FieldRender.vue';
  import ActionRender from './components/ActionRender.vue';

  export default defineComponent({
    inheritAttrs: false,
    props: {
      renderActionVNodes: {
        type: Function,
        required: true
      },
      renderFieldVNodes: {
        type: Function,
        required: true
      },
    showFieldVNode: {
      type: Boolean,
      default: false
    }
    },
    components: {
      FieldRender,
      ActionRender
    }
  });
</script>
<style lang="scss">
  .custom-form-container {
    .custom-form-tip {
      color: var(--oio-primary-color);
      margin-bottom: var(--oio-margin);
      font-size: 24px;
    }
  }
</style>

注册 layout

registerLayout(
  `<view type="FORM">
    <element widget="CustomFormWidget" slot="form">
        <xslot name="fields" slotSupport="pack,field" />
    </element>
  </view>`,
  {
    moduleName: 'moduleName',
    model: 'model',
    viewType: ViewType.Form,
    actionName: 'actionName'
  }
);

自定义表格视图与字段动作的结合

注册表格对应的 SPI

// CustomTableWidget.ts

import {
  BaseElementObjectViewWidgetProps,
  BaseElementWidget,
  DslRender,
  SPI,
  TableWidget,
  ViewType,
  Widget
} from '@kunlun/dependencies';
import CustomTable from './CustomTable.vue';

@SPI.ClassFactory(
  BaseElementWidget.Token({
    viewType: ViewType.Table,
    widget: 'CustomTableWidget'
  })
)
export class CustomTableWidget extends TableWidget {
  public initialize(props: BaseElementObjectViewWidgetProps): this {
    super.initialize(props);
    this.setComponent(CustomTable);
    return this;
  }

  /**
   * 渲染行内动作VNode
   */
  @Widget.Method()
  protected renderRowActionVNodes() {
   // const table = this.metadataRuntimeContext.viewDsl?.widgets.find((w) => w.slot === 'table');

    // const rowAction = table?.widgets.find((w) => w.slot === 'rowActions');
    // this.refreshCallChaining;
    // if (rowAction) {
    //   return rowAction.widgets.map((w) => DslRender.render(w));
    // }

    // return null;

    const modelActions = this.metadataRuntimeContext.model.modelActions;

    if (modelActions.length) {
      return modelActions.map((w) => DslRender.render(w.template!));
    }

    return null;
  }

  /**
   * 渲染动作VNode
   */
  @Widget.Method()
  protected renderActionVNodes() {
    const actionBar = this.metadataRuntimeContext.viewDsl?.widgets.find((w) => w.slot === 'actions');

    if (actionBar) {
      return actionBar.widgets.map((w, index) =>
        DslRender.render({
          ...w,
          __index: index
        })
      );
    }

    return null;
  }
}

在上述代码中,我们继承的是TableWidget,所以页面也会自动发起查询,并且定义了renderRowActionVNodesrenderActionVNodes方法,用于渲染行内动作和动作。

表格视图渲染动作

<!-- CustomTable.vue -->
<template>
  <div class="custom-table-container">
    <div class="custom-table-tip">自定义视图</div>

    <div style="display: flex; gap: 10px">
      <p style="color: red; margin-top: 10px">这是表格的动作</p>
      <action-render :renderActionVNodes="renderActionVNodes"></action-render>
    </div>

    <p style="color: red; margin-top: 10px">这是表格的数据跟行内动作</p>
    <div>
      <div v-for="(row, index) in dataSource" :key="row.index">
        <div style="display: flex">
          <div><span>行数据</span> <span> ID:{{ row.id }}</span></div>
          <div>
            <span>行动作</span>
            <span>
              <row-action-render
                :renderRowActionVNodes="renderRowActionVNodes"
                :row="row"
                :rowIndex="index"
                :parentHandle="currentHandle"
              ></row-action-render>
            </span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script lang="ts">
  import { defineComponent, PropType } from 'vue';
  import ActionRender from './components/ActionRender.vue';
  import RowActionRender from './components/RowActionRender.vue';

  export default defineComponent({
    inheritAttrs: false,
    props: {
        currentHandle: {
          type: String,
          required: true
        },
      renderActionVNodes: {
        type: Function,
        required: true
      },
      renderRowActionVNodes: {
        type: Function,
        required: true
      },
      dataSource: {
        type: Array as PropType<any[]>,
        default: () => []
      }
    },
    components: {
      RowActionRender,
      ActionRender
    }
  });
</script>
<style lang="scss">
  .custom-table-container {
    .custom-table-tip {
      color: var(--oio-primary-color);
      margin-bottom: var(--oio-margin);
      font-size: 24px;
    }
  }
</style>

ActionRender.vue跟表单中的ActionRender.vue一样,内部也是在 render 函数中调用renderActionVNodes

RowActionRender.vue有点特殊,下面是对应的代码(固定写法)。

<script lang="ts">
  import { ActionBar, RowActionBarWidget, uniqueKeyGenerator } from '@kunlun/dependencies';
  import { debounce } from 'lodash-es';
  import { createVNode, defineComponent } from 'vue';

  export default defineComponent({
    inheritAttrs: false,
    props: {
      row: {
        type: Object,
        required: true
      },
      rowIndex: {
        type: Number,
        required: true
      },
      renderRowActionVNodes: {
        type: Function,
        required: true
      },
      parentHandle: {
          type: String,
          required: true
        }
    },
    render() {
      const vnode = this.renderRowActionVNodes();

      return createVNode(
        ActionBar,
        {
          widget: 'rowAction',
          parentHandle: this.parentHandle,
          inline: true,
          activeRecords: this.row,
          rowIndex: this.rowIndex,
          key: this.rowIndex,
          refreshWidgetRecord: debounce((widget?: RowActionBarWidget) => {
            if (widget) {
              widget.setCurrentActiveRecords(this.row);
            }
          })
        },
        {
          default: () => vnode
        }
      );
    }
  });
</script>

注册 layout

import { registerLayout, ViewType } from '@kunlun/dependencies';

registerLayout(
  `<view type="TABLE">
    <element widget="CustomTableWidget" slot="table" slotSupport="field">
            <element widget="expandColumn" slot="expandRow" />
            <xslot name="fields" slotSupport="field" />
            <element widget="rowActions" slot="rowActions" slotSupport="action" />
        </element>
</view>`,
  {
    moduleName: 'resource',
    model: 'resource.ResourceProvince',
    viewType: ViewType.Table
  }
);

本文讲解了自定义视图与界面设计器中配置的字段和动作的无缝对接,以实现自定义视图的功能,本质上是将界面设计器配置的字段、动作,渲染成对应的 VNode,然后在自定义的页面渲染。

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

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

Like (1)
汤乾华's avatar汤乾华数式员工
Previous 2024年9月12日 am11:23
Next 2024年9月12日 pm10:16

相关推荐

  • 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日
    1.3K00
  • 【前端】IOC容器(v4)

    什么是IOC容器? IOC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合,更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IOC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的使程序的整个体系结构变得非常灵活。在运行期,在外部容器动态的将依赖对象注入组件,当外部容器启动后,外部容器就会初始化。创建并管理对象实例,以及销毁,这种应用本身不负责依赖对象的创建和维护,依赖对象的创建和维护是由外部容器负责的称为控制反转。 IOC(控制反转)和DI(依赖注入) IOC(Inversion of Control, 控制反转):通过外部容器管理对象实例的一种思想。DI(Dependency Injection, 依赖注入):IOC的一种实现方式。 作者简述 IOC是Spring框架(一种以Java为语言开发的框架)的核心,并贯穿始终。其面向接口的开发能力,使得服务调用方和服务提供方可以做到完全解耦,只要遵循接口定义的规则进行调用,具体服务的实现可以是多样化的。 对于前端,我们使用inversify进行了IOC的实现。其强大的解耦能力可以使得平台进行大量的抽象,而无需关系具体的实现。 接下来,我们将介绍IOC在开发中的基本运用。 API 为了方便起见,我们将IOC相关功能与组件SPI的调用方式放在了一起。(更高版本的平台版本将自动获得该能力) export class SPI { /** * register singleton service */ public static Service; /** * autowired service property/parameter in service */ public static Autowired; /** * service construct after execute method */ public static PostConstruct; /** * autowired service in widget */ public static Instantiate; /** * autowired services in widget */ public static Instantiates; /** * service construct after execute method in widget */ public static InstantiatePostConstruct; } 创建第一个服务 service/ProductService.ts import { ServiceIdentifier } from '@kunlun/dependencies'; /** * 产品 */ export interface Product { id: string; name: string; } /** * 产品服务 */ export interface ProductService { /** * 获取产品列表 */ getProducts(): Promise<Product[]>; /** * 通过ID获取产品 * @param id 产品ID */ getProductById(id: string): Promise<Product | undefined>; } /** * 产品服务Token */ export const ProductServiceToken = ServiceIdentifier<ProductService>('ProductService'); service/impl/ProductServiceImpl.ts import { SPI } from '@kunlun/dependencies'; import { Product, ProductService, ProductServiceToken } from '../ProductService'; @SPI.Service(ProductServiceToken) export class ProductServiceImpl implements ProductService { public async getProducts(): Promise<Product[]> { // request api get products return []; } public async getProductById(id:…

    前端 2023年11月1日
    1.4K00
  • 「前端」 获取当前用户信息以及语言

    概述 TopBarService 是用于管理用户信息和多语言配置的工具类,提供以下核心功能: 用户信息查询(含缓存机制) 当前语言状态获取 可用语言列表查询 语言切换操作 快速开始 // 基础使用示例 import { TopBarService } from '@kunlun/dependencies'; // 获取用户信息(自动缓存) const userInfo = await TopBarService.getUserInfo(); // 获取当前语言配置 const currentLang = await TopBarService.getCurrentLang(); // 查询支持的语言列表 const languages = await TopBarService.queryLanguageList(); // 激活指定语言 await TopBarService.activeLang('zh_CN'); API 详解 方法功能说明 方法名称 描述 参数 返回值 ​getUserInfo 获取当前用户信息(自动缓存) 无 用户信息对象 ​queryUserInfo 强制获取最新用户信息 无 最新用户信息对象 ​getCurrentLang 获取当前激活的语言配置 无 语言配置对象 / undefined ​queryLanguageList 获取所有可用语言列表 无 语言列表数组 ​activeLang 激活指定语言 语言 ID (string) 更新后的语言配置对象

    2025年3月21日
    2.0K00
  • 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日
    1.1K00
  • 前端自定义组件之单页面步骤条

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

Leave a Reply

Please Login to Comment