前端自定义组件之单页面步骤条

本文将讲解如何通过自定义,实现单页面的步骤条组件。其中每个步骤的元素里都是界面设计器拖出来的。
前端自定义组件之单页面步骤条

实现路径

整体的实现思路是界面设计器拖个选项卡组件,自定义这个选项卡,里面的每个选项页都当成一步渲染出来,每一步的名称是选项页的标题。

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 ?? true;
  }

  // region 数据提交相关

  // 提交数据的方法
  @Widget.Method()
  public async onSave() {
    const submitData = this.formData;
    await customMutation(this.model.model, 'create', submitData || {});
    window.history.back();
  }
}

vue组件核心内容是用component :is属性,渲染出配置的选项页组件

<template>
  <div class="next-step">
    <a-steps :current="current">
      <a-step v-for="title in titles" :title="title" />
    </a-steps>
    <OioSpin :loading="loading" class="oio-spin">
      <div class="step-main">
        <template v-for="(stepEl, index) in stepsVNodes">
          <div v-if="current === index" class="step-item">
            <component :is="stepEl" />
          </div>
        </template>
      </div>
      <div class="step-footer" v-if="stepsVNodes.length">
        <a-button v-if="current > 0" class="oio-button" type="primary" @click="previous"> 上一步 </a-button>
        <a-button v-if="current < stepsVNodes.length - 1" class="oio-button" type="primary" @click="next">
          下一步
        </a-button>
        <a-button v-if="current === stepsVNodes.length - 1" class="oio-button" type="primary" @click="finish">
          完成
        </a-button>
      </div>
    </OioSpin>
  </div>
</template>
<script setup lang="ts">
import { computed, PropType, ref, VNode } from 'vue';
import { OioSpin } from '@oinone/kunlun-vue-ui-antd';
import { DslDefinition } from '@oinone/kunlun-dependencies';

const props = defineProps({
  loading: {
    type: Boolean,
    default: false
  },
  titles: {
    type: Array as PropType<string[]>,
    default: []
  },
  steps: {
    type: Array as PropType<DslDefinition[][]>,
    default: []
  },
  renderStep: {
    type: Function as PropType<(step: DslDefinition[]) => VNode[]>
  },
  onValidator: {
    type: Function
  },
  onSave: {
    type: Function
  }
});

const stepsVNodes = computed(() => {
  return props.steps?.map((step) => props.renderStep?.(step));
});

const current = ref<number>(0);

const next = async () => {
  if (await props.onValidator?.()) {
    current.value++;
  }
};
const previous = async () => {
  // if (await props.onValidator?.()) {
  current.value--;
  // }
};
const finish = async () => {
  if (await props.onValidator?.()) {
    props.onSave?.();
  }
};
</script>
<style lang="scss" scoped>
.next-step {
  height: 100%;
  background-color: #fff;
  padding: 16px;

  .step-main {
    display: flex;
    justify-content: flex-start;

    .step-item {
      width: 100%;
    }
  }

  .step-footer {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    gap: 16px;
    margin-top: 18px;
  }
}
</style>

3. 效果

前端自定义组件之单页面步骤条

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

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

Like (0)
银时's avatar银时数式员工
Previous 2025年7月8日 pm3:43
Next 2025年7月8日 pm5:58

相关推荐

  • oio-pagination 分页

    API 参数 说明 类型 默认值 版本 currentPage(v-model:currentPage) 当前页数 number – defaultPageSize 默认的每页条数 number 15 disabled 禁用分页 boolean – pageSize 每页条数 number – pageSizeOptions 指定每页可以显示多少条 string[] [’10’, ’15’, ’30’, ’50’, ‘100’, ‘200’] showQuickJumper 是否可以快速跳转至某页 boolean false showSizeChanger 是否展示 pageSize 切换器,当 total 大于 50 时默认为 true boolean – total 数据总数 number 0 事件 事件名称 说明 回调参数 change 页码或 pageSize 改变的回调,参数是改变后的页码及每页条数 Function(page, pageSize) noop

    2023年12月18日
    1.9K00
  • 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日
    2.5K00
  • oio-drawer抽屉

    屏幕边缘滑出的浮层面板。 何时使用 抽屉从父窗体边缘滑入,覆盖住部分父窗体内容。用户在抽屉内操作时不必离开当前任务,操作完成后,可以平滑地回到原任务。 当需要一个附加的面板来控制父窗体内容,这个面板在需要时呼出。比如,控制界面展示样式,往界面中添加内容。 当需要在当前任务流中插入临时任务,创建或预览附加内容。比如展示协议条款,创建子对象。 API 参数 说明 类型 默认值 版本 class 对话框外层容器的类名 string – closable 是否显示左上角的关闭按钮 boolean true closeIcon 自定义关闭图标 VNode | slot destroyOnClose 关闭时销毁 Drawer 里的子元素 boolean false footer 抽屉的页脚 VNode | slot – getTriggerContainer 指定 Drawer 挂载的 HTML 节点 HTMLElement | () => HTMLElement | Selectors ‘body’ height 高度, 在 placement 为 top 或 bottom 时使用 string | number keyboard 是否支持键盘 esc 关闭 boolean true mask 是否展示遮罩 Boolean true maskClosable 点击蒙层是否允许关闭 boolean true placement 抽屉的方向 ‘top’ | ‘right’ | ‘bottom’ | ‘left’ ‘right’ style 可用于设置 Drawer 最外层容器的样式,和 drawerStyle 的区别是作用节点包括 mask CSSProperties – title 标题 string | slot – visible(v-model:visible) Drawer 是否可见 boolean – width 宽度 string | number 378 zIndex 设置 Drawer 的 z-index Number 1000 cancelCallback 点击遮罩层或右上角叉或取消按钮的回调, return true则关闭弹窗 function(e) enterCallback 点击确定回调 function(e)

    2023年12月18日
    1.6K00
  • 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日
    2.0K00
  • 前端页面嵌套

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

    2025年7月21日
    1.4K00

Leave a Reply

Please Login to Comment