npm package: @pluve/lego-excel-vue
Description: lego excel
Author: yangwend
Latest version: 0.12.1

# @pluve/lego-excel-vue

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

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

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

## 安装

```bash
# 依赖 vue@3.x/ant-design-vue@3.x/@ant-design/icons-vue@6.x
yarn add @pluve/lego-excel-vue
```

## 公共类型 LegoExcelTypes

#### LegoExcelTypes.TableKey
注意：0.10.0 版本以下为 `ILegoExcelTableKey`。

> 导入数据对象描述

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

#### LegoExcelTypes.Rule
注意：0.10.0 版本以下为 `ILegoExcelRule`。

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

#### LegoExcelTypes.ErrorModalOptions
注意：0.10.0 版本以下不返回。

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


## LegoExcelImport

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

### API

| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| title | 导入模态框标题 | `string` | 批量导入 |
| btnText | 导入按钮文案 | `string` | 批量导入 |
| templateUrl | 导入模版地址，<b>优先级</b>：`download` > `templateUrl` | `string` | - |
| download | 插槽，用于自定义下载导入模板 | `v-slot:download` | - |
| tableKeys | 配置描述 |  `Array<LegoExcelTypes.TableKey>` | - |
| maxLength | 最大一次导入行数 | `number` | - |
| fileSizeLimit | 文件大小限制(M) | `number` | - |
| beforeUploadStart | beforeUpload 前置校验 | `(file: RcFile, FileList: RcFile[]) => Promise<boolean>` | - |
| beforeOk | 点击【确定】按钮前置校验 | `(data: any[]) => Promise<boolean>` | - |
| customRequest | 自定义校验，一般用于业务校验（如调用后端接口校验） | `(data: any[]) => Promise<boolean>` | - |
| default | 默认插槽，用于自定义选择器 (调用 show 打开导入模态框) | `v-slot:default="{ show }"` | - |
| onFinish | 导入完成回调 | `(data: any[]) => void` | - |
| onFileChange | 文件变化的事件 | `(data: { file?: RcFile }) => void` | - |
| onCancel | 导入弹窗关闭事件 | - |
| modalProps | 透传 `ant-design-vue` `Modal` 组件属性 | `interface ILegoExcelImportModalProps extends Omit<ModalProps, "onOk" \| "onCancel" \| "visible" \| "destroyOnClose" \| "closable" \| "keyboard" \| "maskClosable">;` |
| uploadProps | 透传 `ant-design-vue` `Upload` 组件属性 | `interface ILegoExcelImportUploadProps extends Omit<UploadProps, "accept" \| "fileList" \| "maxCount" \| "customRequest" \| "onRemove" \| "beforeUpload">;`
| readExcelOptions | `XLSX.utils.sheet_to_json(worksheet: WorkSheet, opts?: Sheet2JSONOpts)`中的第二个参数，0.10.0 版本以下不支持 | `Sheet2JSONOpts` | - |
| errorModalOptions | `LegoExcelUtils.showErrorByModal(errorList: string[], options: LegoExcelTypes.ErrorModalOptions \| number = 520)` 中的第二个参数，0.10.0 版本以下不支持 | `LegoExcelTypes.ErrorModalOptions` | - |
| syncValidateRules | 是否同步进行规则校验 | `boolean` | `false` |
| transformErrorMessages | 转换错误信息，0.12.0 版本新增 | `(msgs: { rowIndex: number; message: string }[]) => string[]` | - |

### 校验顺序

1. 上传时校验顺序：
   - beforeUploadStart 函数校验
   - 导入文件是否是 xlsx 格式
   - 导入文件大小是否超过 fileSizeLimit M

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

### 效果展示

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

### 使用示例

```vue
<template>
  <div :class="$style.wrap">
    <LegoExcelImport
      templateUrl="https://yf-test-oss.yifengx.com/webtest/pluve/static/lego-excel-import-file.xlsx"
      :tableKeys="CHANNEL_GOODS_TABLE"
      :fileSizeLimit="2"
      :customRequest="customRequest"
      @fileChange="onFileChange"
      @finish="onImportFinish"
      :error-modal-options="{ cancelText: '取消' }"
      :beforeUploadStart="beforeUploadStart"
      :beforeOk="beforeOk"
      :uploadProps="{ disabled: false }"
      :syncValidateRules="true"
    >
      <template #default="{ show }">
        <Button type="primary" @click="show">导入</Button>
      </template>
      <!-- <template #download>
        <Button type="link" size="small" style="padding: 0" @click="downloadTemplateUrl">自定义下载导入模板</Button>
      </template> -->
    </LegoExcelImport>
    <Button type="primary" @click="exportData" style="margin-left: 10px"
      >导出</Button
    >
  </div>
</template>

<script setup lang="ts">
import { Button } from "ant-design-vue";
import { isNumber, toNumber } from "lodash";
import { LegoExcelImport, LegoExcelUtils, LegoExcelTypes } from '@pluve/lego-excel-vue';

// 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[]) =>
    new Promise((resolve) => {
      // 模拟服务端校验
      setTimeout(() => {
        resolve(true);
      }, 500);
    });

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

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

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;
};
</script>

<style lang="less" module>
.wrap {
  position: relative;
  min-height: 52px;
  padding: 10px 10px 0;
  border-radius: 4px;
  margin-top: 10px;
}
</style>
```

## LegoExcelUtils

提供一系列 Excel 相关方法

### showErrorByModal
公用展示错误弹窗

#### Params

> `showErrorByModal(errorList, options)`

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

#### 示例

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

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-vue';

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-vue';

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-vue';

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 的数组`。

#### 示例
```ts
import { LegoExcelUtils } from '@pluve/lego-excel-vue';

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

### xlsx

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