jtwang7 / Project-Note

开发小记 - 项目里的一些新奇玩意儿
1 stars 0 forks source link

本地 `.csv` 文件上传解析管理器实现:csv-loader #12

Open jtwang7 opened 1 year ago

jtwang7 commented 1 year ago

本地 .csv 文件上传解析管理器实现:csv-loader

参考文章:

功能描述

实现一个组件,其能够将本地 .csv 文件解析成对应的前端数据,不经由后台服务器处理。 总结而言就是:前端在线解析csv文件,并回显到前端。

思路

借助 antd<Upload /> 组件实现拖拽上传功能,同时调用 beforeUpload 钩子取消 antd 默认的文件上传逻辑。 借助 PapaParse.js 在前端解析 .csv 文件。

代码

import { InboxOutlined } from "@ant-design/icons";
import { useBoolean } from "ahooks";
import { Upload, UploadProps } from "antd";
import { RcFile } from "antd/es/upload";
import _ from "lodash";
import Papa from "papaparse";
import { useEffect, useState } from "react";
import { Data, FileLoaderProps } from "./type";

export default function FileLoader(props: FileLoaderProps) {
  const { style, className, onChange, onLoading, onCompleted } = props;

  const { Dragger } = Upload;

  // 数据
  const [data, setData] = useState<Data | null>(null);
  // 加载状态
  const [loading, { setTrue, setFalse }] = useBoolean(false);
  // 是否加载完毕
  const [completed, { setTrue: setCompleted, setFalse: setInitCompleted }] =
    useBoolean(false);

  const uploadProps: UploadProps = {
    accept: ".csv",
    directory: false, // 以文件 or 文件夹形式上传
    multiple: true, // 是否支持多文件上传
    showUploadList: false, // 是否展示上传文件的列表
    beforeUpload: (file, fileList) => {
      const parser = (file: RcFile | RcFile[]) => {
        // 闭包保存当前的解析进度
        const total = Array.isArray(file) ? file.length : 1;
        let completed = 0;
        return function fn(data = file) {
          // fn 用于解析文件
          if (Array.isArray(data)) {
            // 若传入多文件,则递归解析
            for (const f of data as RcFile[]) {
              fn(f);
            }
          } else {
            // 解析单文件
            const f = data as RcFile;
            Papa.parse(f, {
              encoding: "utf-8",
              dynamicTyping: true, // 类型解析: string -> number / boolean
              header: true, // 保留 csv header
              complete(results, file) {
                const res = results.data;
                //去除最后的空行 有些解析数据尾部会多出空格
                if (res[res.length - 1] === "") {
                  res.pop();
                }
                setData((prev) => {
                  prev = !prev ? ({} as Data) : prev;
                  completed += 1;
                  Reflect.set(prev, f.name.split(".")[0], res);
                  return _.cloneDeep(prev);
                });
                // 数据加载完毕
                if (completed === total) {
                  setFalse();
                  setCompleted();
                }
              },
            });
          }
        };
      };
      // 开始加载数据 - 初始化状态及数据
      setTrue();
      setInitCompleted();
      setData(null);
      fileList.length ? parser(fileList)() : parser(file)();
      // 中断 antd Upload 文件上传
      return false;
    },
  };

  useEffect(() => {
    onChange?.(data);
  }, [data]);

  useEffect(() => {
    onLoading?.(loading);
  }, [loading]);

  useEffect(() => {
    onCompleted?.(completed);
  }, [completed]);

  return (
    <Dragger
      {...uploadProps}
      style={{ width: "100%", height: "100%", ...style }}
      className={className}
    >
      <p className="ant-upload-drag-icon">
        <InboxOutlined />
      </p>
      <p className="ant-upload-text">
        Click or drag file to this area to upload
      </p>
      <p className="ant-upload-hint">
        Support for a single or bulk upload. Strictly prohibit from uploading
        company data or other band files
      </p>
    </Dragger>
  );
}
import { CSSProperties } from "react";

export type Data = { [key: string]: any[] };

export type FileLoaderProps = {
  style?: CSSProperties;
  className?: string;
  onChange?: (data: Data) => void;
  onLoading?: (status: boolean) => void;
  onCompleted?: (status: boolean) => void;
};