前端表格复制

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

代码实现

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

相关推荐

  • 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日
    98800
  • 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日
    75800
  • 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.3K00
  • 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日
    1.3K00
  • oio-grid 栅格

    24 栅格系统。 <oio-row :gutter="24"> <oio-col :span="12"></oio-col> <oio-col :span="12"></oio-col> </oio-row> 概述 布局的栅格化系统,我们是基于行(row)和列(col)来定义信息区块的外部框架,以保证页面的每个区域能够稳健地排布起来。下面简单介绍一下它的工作原理: 通过\row\在水平方向建立一组\column\(简写 col) 你的内容应当放置于\col\内,并且,只有\col\可以作为\row\的直接元素 栅格系统中的列是指 1 到 24 的值来表示其跨越的范围。例如,三个等宽的列可以使用 \<a-col :span="8" />\ 来创建 如果一个\row\中的\col\总和超过 24,那么多余的\col\会作为一个整体另起一行排列 Flex 布局 我们的栅格化系统支持 Flex 布局,允许子元素在父节点内的水平对齐方式 – 居左、居中、居右、等宽排列、分散排列。子元素与子元素之间,支持顶部对齐、垂直居中对齐、底部对齐的方式。同时,支持使用 order 来定义元素的排列顺序。 Flex 布局是基于 24 栅格来定义每一个『盒子』的宽度,但不拘泥于栅格。 API Row 成员 说明 类型 默认值 align flex 布局下的垂直对齐方式:top middle bottom string top gutter 栅格间隔,可以写成像素值或支持响应式的对象写法来设置水平间隔 { xs: 8, sm: 16, md: 24}。或者使用数组形式同时设置 [水平间距, 垂直间距](1.5.0 后支持)。 number/object/array 0 justify flex 布局下的水平排列方式:start end center space-around space-between string start wrap 是否自动换行 boolean false Col 成员 说明 类型 默认值 版本 flex flex 布局填充 string|number – offset 栅格左侧的间隔格数,间隔内不可以有栅格 number 0 order 栅格顺序,flex 布局模式下有效 number 0 pull 栅格向左移动格数 number 0 push 栅格向右移动格数 number 0 span 栅格占位格数,为 0 时相当于 display: none number – xxxl ≥2000px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – xs <576px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – sm ≥576px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – md ≥768px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – lg ≥992px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – xl ≥1200px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object – xxl ≥1600px 响应式栅格,可为栅格数或一个包含其他属性的对象 number|object –

    2023年12月18日
    97800

Leave a Reply

登录后才能评论