前端表格复制

我们可能会遇到表格复制的需求,也就是表格填写的时候,不是增加一行数据,而是增加一个表格。
前端表格复制
以下是代码实现和原理分析。

代码实现

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) as DslDefinition | undefined;
      this.currentViewDsl = viewDsl;
    }
    const runtimeSubview = this.generatorRuntimeSubview(props);
    this.initRuntimeContext(widget, runtimeSubview as RuntimeView);
    this.initSubviewAfterProperties(props);
  }

  // mountedProcess 里数据已经回填,根据值动态创建表格
  protected async mountedProcess() {
    await super.mountedProcess();
    if (Array.isArray(this.value) && this.value.length > 0) {
      this.value.forEach((item) => {
        this.createTableWidget(item);
      });
    } else {
      this.createTableWidget({});
    }
  }

  // region 删除动态表格

  @Widget.Method()
  public async deleteTableWidget(index) {
    this.myMetadataViewWidget.splice(index, 1);
    this.myMetadataViewWidgetKeys.splice(index, 1);
    this.myMetadataViewWidgetLength--;
  }

  // region 数据提交

  public async submit(submitValue: SubmitValue) {
    // 拿到所有子表格的数据
    const records = this.myMetadataViewWidget.map((widget) => widget.dataSource?.[0]).filter((record) => !!record);
    const returnValue = {};
    returnValue[this.itemName] = records;
    return returnValue;
  }
}

DynamicCreateTable.vue 动态添加表格 vue 组件

<template>
  <div class="dynamic-create-table" v-bind="basicProps">
    <div class="dynamic-create-table-container">
      <oio-icon icon="oinone-tianjia2" size="24" @click="createTableWidget({})" />
    </div>
    <template v-for="(key, index) in myMetadataViewWidgetKeys" :key="key">
      <div class="dynamic-delete-table-container">
        <oio-icon icon="oinone-shanchu" size="24" @click="deleteTableWidget(index)" />
        <slot :name="key" />
      </div>
    </template>
  </div>
</template>

<script lang="ts">
import { OioIcon, PropRecordHelper } from '@oinone/kunlun-dependencies';
import { computed, defineComponent, PropType } from 'vue';

export default defineComponent({
  name: 'DynamicCreateTable',
  inheritAttrs: false,
  components: { OioIcon },
  props: {
    myMetadataViewWidgetLength: {
      type: Number
    },
    myMetadataViewWidgetKeys: {
      type: Array as PropType<string[]>
    },
    createTableWidget: {
      type: Function,
      default: () => {}
    },
    deleteTableWidget: {
      type: Function,
      default: () => {}
    }
  },
  setup(props, context) {
    const basicProps = computed(() => {
      return PropRecordHelper.collectionBasicProps(context.attrs, [`inline-table`]);
    });

    return {
      basicProps
    };
  }
});
</script>
<style lang="scss">
.dynamic-create-table {
  display: flex;
  flex-direction: column;
  gap: 24px;

  > .dynamic-create-table-container {
    display: flex;
    justify-content: flex-end;

    > .oio-icon {
      cursor: pointer;
    }
  }

  > .dynamic-delete-table-container {
    position: relative;

    > .oio-icon {
      position: absolute;
      z-index: 1;
      right: 0;
      top: -12px;
      cursor: pointer;
    }
  }

  &.inline-table .oio-default-table-view {
    min-height: unset;
  }
}
</style>

MyMetadataViewWidget 数据隔离组件

import { ActiveRecord, ActiveRecords, CallChaining, MetadataViewWidget, Widget } from '@oinone/kunlun-dependencies';

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();
  }

  public getFormData() {
    return this.activeRecords?.[0];
  }

  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 || ''}]`;
  }
}

原理分析

参考 https://doc.oinone.top/frontend/view-api/21426.html

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

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

(0)
银时的头像银时数式员工
上一篇 2025年7月21日 am11:18
下一篇 2025年7月21日 pm5:49

相关推荐

  • 前端页面嵌套

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

    全局展示操作反馈信息。 何时使用 可提供成功、警告和错误等反馈信息。 顶部居中显示并自动消失,是一种不打断用户操作的轻量级提示方式。 API 组件提供了一些静态方法,使用方式和参数如下: OioMessage.success(title, options) OioMessage.error(title, options) OioMessage.info(title, options) OioMessage.warning(title, options) options 参数如下: 参数 说明 类型 默认值 版本 duration 默认 3 秒后自动关闭 number 3 class 自定义 CSS class string –

    2023年12月18日
    96000
  • 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日
    67700

Leave a Reply

登录后才能评论