npm package: @pluve/lego-excel-react
Description: lego excel components with react
Author: 杨雯
Latest version: 0.3.14

# @pluve/lego-excel-react

乐高系列之 `excel` 文件处理

[![npm (scoped)](https://img.shields.io/npm/v/@pluve/lego-excel-react?style=for-the-badge)](https://www.npmjs.com/package/@pluve/lego-excel-react)

`@pluve/lego-excel-react` 已经投入了我们的生产环境中使用，经受住了来自真实业务的考验，并伴随着我们的业务需求不断完善。

## 安装

```bash
# npm
npm i @pluve/lego-excel-react

# yarn
yarn add @pluve/lego-excel-react
```

## 公共类型 LegoExcelTypes

### LegoExcelTypes.TableKey

> 导入数据对象描述

| 参数      | 说明                                  | 类型                                |
| --------- | ------------------------------------- | ----------------------------------- |
| label     | 表头列名称                            | `string`                            |
| value     | 表头列属性（表头列名称所对应的 key）  | `string`                            |
| rules     | 列规则校验                            | `Array<LegoExcelTypes.Rule>`        |
| formatter | 格式化该列数据（在解析 excel 时执行） | `(value?: string \| number) => any` |

### LegoExcelTypes.Rule

| 参数      | 说明                                                                                                | 类型                                                                           |
| --------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| required  | 是否必填                                                                                            | `boolean`                                                                      |
| pattern   | 正则校验                                                                                            | `RegExp`                                                                       |
| message   | 错误提示                                                                                            | `string`                                                                       |
| validator | 自定义函数校验，`syncValidateRules`为 `true` 时返回 `boolean`，为 `false` 时返回 `Promise<boolean>` | `(value: any, rowData: { [key: string]: any }) => Promise<boolean> \| boolean` |

### LegoExcelTypes.ErrorModalOptions

```ts
export type ErrorModalOptions = Omit<ModalFuncProps, 'content'>;
```

## LegoExcelImport

通常用于批量导入数据场景

### API

| 参数              | 说明                                                                                                                             | 类型                                                                                                                                                               | 默认值   |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |
| title             | 导入模态框标题                                                                                                                   | `string \| React.ReactNode`                                                                                                                                        | 批量导入 |
| btnText           | 导入按钮文案                                                                                                                     | `string`                                                                                                                                                           | 批量导入 |
| renderBtn         | 自定义导入按钮(调用 `show` 打开导入模态框)                                                                                       | `(params: { show: () => void }) => React.ReactNode`                                                                                                                | -        |
| templateUrl       | 导入模版地址，<b>优先级</b>：`download` > `templateUrl`                                                                          | `string`                                                                                                                                                           | -        |
| download          | 自定义下载导入模板                                                                                                               | `React.ReactNode`                                                                                                                                                  | -        |
| tableKeys         | 配置描述                                                                                                                         | `Array<LegoExcelTypes.TableKey>`                                                                                                                                   | -        |
| maxLength         | 最大一次导入行数                                                                                                                 | `number`                                                                                                                                                           | -        |
| fileSizeLimit     | 文件大小限制(M)                                                                                                                  | `number`                                                                                                                                                           | -        |
| size              | 导入弹窗内元素的尺寸（不包含导入按钮的尺寸）                                                                                     | `small \| middle \| large \| `                                                                                                                                     | middle   |
| beforeUploadStart | beforeUpload 前置校验                                                                                                            | `(file: RcFile, FileList: RcFile[]) => Promise<boolean>`                                                                                                           | -        |
| beforeOk          | 点击【确定】按钮前置校验                                                                                                         | `(data: any[]) => Promise<boolean>`                                                                                                                                | -        |
| customRequest     | 自定义校验，一般用于业务校验（如调用后端接口校验）                                                                               | `(data: any[]) => Promise<boolean>`                                                                                                                                | -        |
| onFinish          | 导入完成回调                                                                                                                     | `(data: any[]) => void`                                                                                                                                            | -        |
| onFileChange      | 文件变化的事件                                                                                                                   | `(data: { file?: RcFile }) => void`                                                                                                                                | -        |
| onCancel          | 导入弹窗关闭事件                                                                                                                 | `() => void`                                                                                                                                                       | -        |
| modalProps        | 透传 `antd` `Modal` 组件属性                                                                                                     | `interface ILegoExcelImportModalProps extends Omit<ModalProps, "onOk" \| "onCancel" \| "open" \| "destroyOnClose" \| "closable" \| "keyboard" \| "maskClosable">;` | -        |
| uploadProps       | 透传 `antd` `Upload` 组件属性                                                                                                    | `interface ILegoExcelImportUploadProps extends Omit<UploadProps, "accept" \| "fileList" \| "maxCount" \| "customRequest" \| "onRemove" \| "beforeUpload">;`        | -        |
| bodyTop           | 导入文件上面自定义内容                                                                                                           | `React.ReactNode`                                                                                                                                                  | -        |
| bodyBottom        | 导入文件下面自定义内容                                                                                                           | `React.ReactNode`                                                                                                                                                  | -        |
| loadingTip        | loading 展示文案                                                                                                                 | `React.ReactNode`                                                                                                                                                  | -        |
| readExcelOptions  | `XLSX.utils.sheet_to_json(worksheet: WorkSheet, opts?: Sheet2JSONOpts)`中的第二个参数                                            | `Sheet2JSONOpts`                                                                                                                                                   | -        |
| errorModalOptions | `LegoExcelUtils.showErrorByModal(errorList: string[], options: LegoExcelTypes.ErrorModalOptions \| number = 520)` 中的第二个参数 | `LegoExcelTypes.ErrorModalOptions`                                                                                                                                 | -        |
| syncValidateRules | 是否同步进行规则校验                                                                                                             | `boolean`                                                                                                                                                          | `false`  |

### 校验顺序

1. 上传时校验顺序：

   - beforeUploadStart 函数校验
   - 导入文件是否是 xlsx 格式
   - 导入文件大小是否超过 fileSizeLimit M

2. 点击确定时校验顺序：
   - beforeOk 函数校验
   - 上传表格是否无数据
   - 表头校验
   - 最大限制一次导入 maxLength 条数据
   - 单元格必填等数据校验
   - 自定义校验

### 效果展示

![demo 截图](https://yf-test-oss.yifengx.com/webtest/pluve/static/lego-excel-import-demo-react.gif)

### 使用示例

```tsx
import {
  LegoExcelImport,
  LegoExcelTypes,
  LegoExcelUtils,
} from '@pluve/lego-excel-react';
import { Button } from 'antd';
import { SizeType } from 'antd/es/config-provider/SizeContext';
import { isNumber, toNumber } from 'lodash-es';
import { FC, useState } from 'react';

const Demo: FC = () => {
  const [size, setSize] = useState<SizeType>('middle');
  // excel 单元格转为数字
  const convertExcelCellToNumber = (value?: string | number) =>
    isNumber(value) ? value : !!value ? toNumber(value) : undefined;

  const CHANNEL_GOODS_TABLE: LegoExcelTypes.TableKey[] = [
    {
      label: '*商品编码',
      value: 'goodsCode',
      rules: [{ required: true, message: '请填写商品编码' }],
      formatter: (value) => `${value}`,
    },
    {
      label: '*商品名称',
      value: 'goodsName',
      rules: [{ required: true, message: '请填写商品名称' }],
      formatter: (value) => `${value}`,
    },
    {
      label: '*供货价',
      value: 'oldSaleAmount',
      rules: [
        { required: true, message: '请填写供货价' },
        {
          pattern:
            /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^0\.0[1-9])|(^0\.[1-9]([0-9])?$)/,
          message: '限制正两位小数',
        },
        {
          validator: (val) => {
            if (val > 999999999.99) {
              return false;
            }
            return true;
          },
          message: '供货价输入不合法',
        },
      ],
      formatter: convertExcelCellToNumber,
    },
    {
      label: '*上架状态',
      value: 'isShelves',
      rules: [{ required: true, message: '请选择上架状态' }],
      formatter: (value) => `${value}`,
    },
  ];

  const customRequest = (data: any[]): Promise<boolean> =>
    new Promise((resolve) => {
      // 模拟服务端校验
      setTimeout(() => {
        resolve(true);
      }, 500);
    });

  const onImportFinish = (data: any[]) => {
    console.log('获取导入数据 --> ', data);
  };

  const onFileChange = (data: { file?: File }) => {
    console.log('监听文件变化 --> ', data.file);
  };

  const toggleSize = (size) => {
    setSize(size);
  };

  const exportData = () => {
    const headers = [
      {
        key: 'no',
        title: '序号',
      },
      {
        key: 'name',
        title: '姓名',
      },
      {
        key: 'age',
        title: '年龄',
      },
      {
        key: 'desc',
        title: '描述',
      },
    ];
    const data = [
      {
        no: 1,
        name: 'john',
        age: 12,
        desc: '我是一个随机的描述我是一个随机的描述我是一个随机的描述我是一个随机的描述',
      },
      {
        no: 2,
        name: 'lucy',
        age: 19,
      },
    ];
    for (let index = 0; index < 10000; index++) {
      data.push({
        no: index + 3,
        name: Math.random() + '',
        age: Math.floor(Math.random() * 100),
        desc: Math.random() > 0.5 ? '开' : '关',
      });
    }
    LegoExcelUtils.exportExcel({
      headers,
      data,
      fileName: '测试导出.xlsx',
      colWidths: [{ wpx: 50 }, { wpx: 50 }, { wpx: 50 }, { wpx: 130 }],
    });
  };

  const downloadTemplateUrl = () => {
    LegoExcelUtils.exportExcel({
      headers: [
        {
          key: 'storeCode',
          title: '*门店编码',
        },
      ],
      data: [
        {
          storeCode: 'xxxx',
        },
      ],
      fileName: '通用门店导入模板.xlsx',
    });
  };

  const beforeUploadStart = async () => {
    console.log('1111');
    return true;
  };

  const beforeOk = async (data: any[]) => {
    console.log('获取导入数据 --> ', data);
    return true;
  };

  return (
    <div style={{ width: '100%' }}>
      <p>
        <Button
          type={size === 'small' ? 'primary' : 'default'}
          onClick={() => toggleSize('small')}
        >
          small
        </Button>
        <Button
          type={size === 'middle' ? 'primary' : 'default'}
          onClick={() => toggleSize('middle')}
        >
          middle
        </Button>
        <Button
          type={size === 'large' ? 'primary' : 'default'}
          onClick={() => toggleSize('large')}
        >
          large
        </Button>
      </p>
      <LegoExcelImport
        templateUrl="https://yf-test-oss.yifengx.com/webtest/pluve/static/lego-excel-import-file.xlsx"
        tableKeys={CHANNEL_GOODS_TABLE}
        fileSizeLimit={2}
        customRequest={customRequest}
        onFileChange={onFileChange}
        onFinish={onImportFinish}
        renderBtn={({ show }) => {
          return <Button onClick={show}>导入</Button>;
        }}
        size={size}
        errorModalOptions={{ cancelText: '取消' }}
        download={
          <Button
            type="link"
            style={{ padding: 0 }}
            onClick={downloadTemplateUrl}
          >
            自定义下载导入模板
          </Button>
        }
        beforeUploadStart={beforeUploadStart}
        beforeOk={beforeOk}
        uploadProps={{ disabled: false }}
        syncValidateRules={true}
      ></LegoExcelImport>
      <Button onClick={() => exportData()} style={{ marginLeft: 10 }}>
        导出
      </Button>
    </div>
  );
};

export default Demo;
```

## LegoExcelUtils

提供一系列 Excel 相关方法

### showErrorByModal

公用展示错误弹窗

#### Params

> `showErrorByModal(errorList, options)`

| 参数      | 说明                        | 类型                                               | 默认值 |
| --------- | --------------------------- | -------------------------------------------------- | ------ |
| errorList | 需要展示的错误信息          | `string[]`                                         | -      |
| options   | 错误弹窗 Modal 的额外 props | `LegoExcelTypes.ErrorModalOptions \| number = 520` | -      |

#### 示例

```ts
import { LegoExcelUtils } from '@pluve/lego-excel-react';

const errorMessages = [
  {
    rowIndex: 1,
    message: '某某某不存在',
  },
];

LegoExcelUtils.showErrorByModal(
  errorMessages.map((item) => `第${item.rowIndex}行：${item.message}`),
  { cancelText: '取消' },
);
```

### validateFields

用于读取到 Excel 数据后的校验（异步）。若 `tableKeys` 中的 `rules` 规则存在异步校验，则使用该方式。

#### Params

> `validateFields({ data, tableKeys })`

| 参数      | 说明                 | 类型                             | 默认值 |
| --------- | -------------------- | -------------------------------- | ------ |
| data      | 表格数据（不含表头） | `any[]`                          | -      |
| tableKeys | 配置描述             | `Array<LegoExcelTypes.TableKey>` | -      |

#### 示例

```ts
import { LegoExcelUtils, LegoExcelTypes } from '@pluve/lego-excel-react';

const result: {
  rowIndex: number; // 行数
  message: string; // 该行所有错误信息（多条以；分割）
}[] = await LegoExcelUtils.validateFields(params: {
  data: any[];
  tableKeys: LegoExcelTypes.TableKey[];
});
```

### validateFieldsSync

用于读取到 Excel 数据后的校验（同步）。若 `tableKeys` 中的 `rules` 规则全为同步校验，则使用该方式。

#### Params

> `validateFieldsSync({ data, tableKeys })`

| 参数      | 说明                 | 类型                             | 默认值 |
| --------- | -------------------- | -------------------------------- | ------ |
| data      | 表格数据（不含表头） | `any[]`                          | -      |
| tableKeys | 配置描述             | `Array<LegoExcelTypes.TableKey>` | -      |

#### 示例

```ts
import { LegoExcelUtils, LegoExcelTypes } from '@pluve/lego-excel-react';

const result: {
  rowIndex: number; // 行数
  message: string; // 该行所有错误信息（多条以；分割）
}[] = LegoExcelUtils.validateFieldsSync(params: {
  data: any[];
  tableKeys: LegoExcelTypes.TableKey[];
});
```

### readExcel

读取 Excel 文件

#### Params

> `readExcel({fileReaderRes, tableKeys, options})`

| 参数          | 说明                                                                                  | 类型                             | 默认值 |
| ------------- | ------------------------------------------------------------------------------------- | -------------------------------- | ------ |
| fileReaderRes | -                                                                                     | `any`                            | -      |
| tableKeys     | 配置描述                                                                              | `Array<LegoExcelTypes.TableKey>` | -      |
| options       | `XLSX.utils.sheet_to_json(worksheet: WorkSheet, opts?: Sheet2JSONOpts)`中的第二个参数 | `Sheet2JSONOpts`                 | -      |

#### Result

| 参数    | 说明                 | 类型            | 默认值 |
| ------- | -------------------- | --------------- | ------ |
| data    | 表格数据（不含表头） | `Array`         | -      |
| headers | 表头数据             | `Array<String>` | -      |

#### 示例

```ts
import { LegoExcelUtils } from '@pluve/lego-excel-react';

const reader = new FileReader();
reader.readAsBinaryString(file);
reader.onload = (e) => {
  const result = e.target?.result;
  if (!!result) {
    // 读取excel
    const excelRes = LegoExcelUtils.readExcel({
      fileReaderRes: result,
      tableKeys: tableKeys,
      options: { raw: true }, // 可选
    });
  }
};
```

### exportExcel

前端导出 Excel 文件。

#### Params

> `exportExcel({headers, data, fileName, colWidths})`

| 参数      | 说明                        | 类型                                                    | 默认值      |
| --------- | --------------------------- | ------------------------------------------------------- | ----------- |
| headers   | Excel 表头描述              | `{ key: string; title: string; [key: string]: any; }[]` | -           |
| data      | 需要导出的数据              | `any[]`                                                 | -           |
| fileName  | 导出 excel 的文件名         | `string`                                                | `导出.xlsx` |
| colWidths | 导出 excel 每一列宽度的集合 | `{ wpx: number }[]`                                     | -           |

**注意：** 若 `colWidths` 未设置，则 `exportExcel` 方法中默认设置为 `wpx 为 120，length 为 headers.length 的数组`。

#### 示例

```tsx
import { LegoExcelUtils } from '@pluve/lego-excel-react';
import { Button } from 'antd';
import { FC } from 'react';

const Demo: FC = () => {
  const exportData = () => {
    const headers = [
      {
        key: 'no',
        title: '序号',
      },
      {
        key: 'name',
        title: '姓名',
      },
      {
        key: 'age',
        title: '年龄',
      },
      {
        key: 'desc',
        title: '描述',
      },
    ];
    const data = [
      {
        no: 1,
        name: 'john',
        age: 12,
        desc: '我是一个随机的描述我是一个随机的描述我是一个随机的描述我是一个随机的描述',
      },
      {
        no: 2,
        name: 'lucy',
        age: 19,
      },
    ];
    for (let index = 0; index < 10000; index++) {
      data.push({
        no: index + 3,
        name: Math.random() + '',
        age: Math.floor(Math.random() * 100),
        desc: Math.random() > 0.5 ? '开' : '关',
      });
    }
    LegoExcelUtils.exportExcel({
      headers,
      data,
      fileName: '测试导出.xlsx',
      // 可传可不传，不传则取每一列 wpx 为 100
      colWidths: [{ wpx: 50 }, { wpx: 50 }, { wpx: 50 }, { wpx: 130 }],
    });
  };

  return (
    <div style={{ width: '100%' }}>
      <Button onClick={exportData}>点我导出</Button>
    </div>
  );
};

export default Demo;
```

### xlsx

同 [xlsx](https://docs.sheetjs.com/docs/)