【WEB】Antd-Pro 表单通过 excel 复制粘贴快速录入小功能

255 阅读3分钟

题记

简单记录下,最近有这么一个小功能:

以下是一个录入的动态表单,通过ProFormList实现。

现在业务人员嫌这个录入太慢了,希望通过 excel达到快速录入的效果。很多时候大家都是下载模板,上传表格来实现的。但是这个表单,除了这个动态表还有很多额外的其他字段,而且不希望固定成一个录入模板。

所以我想着直接从 excel复制数据,然后自动录入,自动新增行不就得了。

image.png

实现效果

20231226113543_rec_.gif

思考

通过从 excel 复制数据,可以得到这样的数据。'1\t2\t3\n4\t5\t6\n7\t\t\n8\t\t',其中,每一列通过\t换行符分隔,每一行通过\n换行符分隔。

image.png

数据拿到了。接下来我们只需要获取到复制的数据,然后拆分每一个单元格数据到对应的格子上就好了。 所以我们要解决下面几个问题:

  1. 监听复制粘贴事件,拿到对应的复制数据,获取当前聚焦的输入框
  2. 获取表单数据,具体是复制到第几行第几列
  3. 自动填充到对应的单元格,自动新增行

实现

1、监听粘贴事件,拿到复制数据

我们通过 windowpaste事件事件就可以拿到了。

其中:ev.clipboardData?.getData("text/plain") 是拿到粘贴板的数据。

PS:记得阻止默认的复制粘贴行为: ev.preventDefault();

  useEffect(() => {
    const onPaste = (ev: ClipboardEvent) => {
      ///粘贴事件ev
      
      //复制数据
      ev.clipboardData?.getData("text/plain")
    };
    window.addEventListener("paste", onPaste);

    return () => {
      window.removeEventListener("paste", onPaste);
    };
  }, []);

2、获取表单数据,具体是复制到第几行第几列

通过 antd 生成的表单元素可以发现,元素的 id 构成为 ${namePath}_${第几行}_${字段名}构成。 而粘贴事件的 ev.target就是当前的聚焦输入框。

image.png

然后我们只需要通过传入 form和监听的表单的 NamePath计算下就出来了。

    //剪切事件的 ev.target 就是当前的聚焦输入框
    const id = `${ev.target?.["id"]}`;
    //获取动态表单对象
    const fieldValue = form.getFieldValue(filedNamePath);
    const idPrefix = filedNamePath.join("_");
    //获取属性名和行数
    const idSubstring = id.slice(idPrefix.length + 1);
    const ids = idSubstring.split("_");
    //拿到行号
    const lineNum = +ids[0];
    //拿到当前聚焦的属性名
    const propsName = ids.slice(1).join("_");

3.解析粘贴数据,设置到对应的单元格

     //粘贴数据
     const data = ev.clipboardData?.getData("text/plain");
     //所有的属性:按照输入框顺序和对应排列
     const allProps = ['name','age'....等等其他表单字段名];
     const indexName = allProps.indexOf(propsName);
     //列数据
      const columnData = data?.split("\n");
      columnData?.forEach((column, i) => {
        //行数据
        const lineDatas = column.split("\t");
        lineDatas.forEach((line, indexLine) => {
          const n = allProps[indexName + indexLine];
          if (n) {
            //如果不存在行,则新增行
            fieldValue[lineNum + i] ??= {};
            //设置行数据到对应的单元格,这里其实还可以加数据校验等等逻辑
            fieldValue[lineNum + i][n] = line;
          }
        });
      });
      //更新表单数据
      form.setFieldValue(filedNamePath, fieldValue);

以上为基本思路。

完整例子

  1. 将以上逻辑抽离成公共函数,方便其他地方使用。

以下为完整逻辑。

/**
 * 录入表格新增复制粘贴效果
 * 需注意:props 中不要带下划线,会影响判断
 *
 * @param form antd 的 form 表单
 * @param filedNamePath 属性名称
 * @param props 属性内容,按表单顺序排序
 */
export function onClipboardFormData(
  ev: ClipboardEvent,
  form: FormInstance,
  filedNamePath: string[],
  props: string[]
) {
  const id = `${ev.target?.["id"]}`;

  const idPrefix = filedNamePath.join("_");
  if (id.startsWith(idPrefix)) {
    ev.preventDefault();
    const fieldValue = form.getFieldValue(filedNamePath);
    //获取属性名和行数
    const idSubstring = id.slice(idPrefix.length + 1);
    const ids = idSubstring.split("_");
    const lineNum = +ids[0];
    const propsName = ids.slice(1).join("_");
    const data = ev.clipboardData?.getData("text/plain");
    //所有的属性:按照输入框顺序和对应排列
    const allProps = props;
    const indexName = allProps.indexOf(propsName);
    if (indexName != -1) {
      //列数据
      const columnData = data?.split("\n");
      columnData?.forEach((column, i) => {
        //行数据
        const lineDatas = column.split("\t");
        lineDatas.forEach((line, indexLine) => {
          const n = allProps[indexName + indexLine];
          if (n) {
            fieldValue[lineNum + i] ??= {};
            fieldValue[lineNum + i][n] = line;
          }
        });
      });
      form.setFieldValue(filedNamePath, fieldValue);
    }
  }
}

  1. 在需要用到的动态表单处使用
const [form] = Form.useForm();

useEffect(() => {
    const onPaste = (ev: ClipboardEvent) => {
      onClipboardFormData(
        ev,
        //Antd表单实例化对象
        form, 
        // 表单属性路径
        ["default", "materials"],  
         //动态属性名,按照显示排序
        ["name", "spec", "material_quality", "process", "color", "quantity"]
      );
    };
    window.addEventListener("paste", onPaste);

    return () => {
      window.removeEventListener("paste", onPaste);
    };
  }, []);

  1. 最终效果

20231226113543_rec_.gif

Bye~