使用 XLSX 和 Ant Design 实现 Excel 文件上传、解析及导出

823 阅读5分钟

在现代 web 应用中,处理 Excel 文件的上传、解析和导出是一个常见的需求。本文将展示如何使用 xlsx 库和 Ant Design 来实现这一功能。我们将分步介绍如何创建一个工具类来处理 Excel 文件,并提供一个示例函数来展示如何使用该工具类上传和解析 Excel 文件。

项目依赖

首先,我们需要安装必要的依赖项:

npm install xlsx 

以下是我公司后台的一个页面,可以看到有上传和导出功能,大部分业务逻辑都是前端这块实现的,比如解析Excel处理成后端需要的json格式基于表格数据导出Excel

image.png

功能

由于该系统其他几个板块也有导出导入功能,所以也是基于XLSX提取了一个工具类:

  • 支持后端提供的下载链接导出Excel
  • 前端上传Excel并解析Excel,并处理成后端需要的json。【因为大部分在本地的Excel的标头是中文的,而数据库存储的是英文字段,需要map一下】
  • 支持根据表格数据导出Excel
  • 支持合并单元格

完整工具类

那么我们创建一个名为 XlsxTools 的工具类,其中包含处理 Excel 文件的各种方法

import * as XLSX from 'xlsx';
import { StringTools } from './string.tools';
import { message } from 'antd';

export type ExcelRecord = Record<string, any>;

export interface ExportToExcelOptions<T extends ExcelRecord> {
    data: T[];
    fileName?: string;
    columnWidths?: number[];
    mergeColumns?: string[];
}

class XlsxTools {
    /**
     * 使用下载链接导出文件
     * @param link 下载链接
     */
    static exportByDownLoadLink(link: string) {
        const date = StringTools.formatTime();
        const a = document.createElement('a');
        a.href = link;
        a.download = `${date}.xlsx`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    }

    /**
     * 导出数组数据为 Excel 文件
     * @param options 导出选项
     */
    static exportByArray<T extends ExcelRecord>(options: ExportToExcelOptions<T>) {
        const { data, fileName = StringTools.formatTime(), columnWidths = [], mergeColumns = [] } = options;
        const wb = XLSX.utils.book_new();
        const ws = XLSX.utils.json_to_sheet(data);
        // 设置列宽
        ws['!cols'] = XlsxTools.setColumnWidths(columnWidths, data);
        // 合并单元格
        if (mergeColumns.length) {
            ws['!merges'] = XlsxTools.mergeRanges(data, mergeColumns);
        }
        XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
        XLSX.writeFile(wb, `${fileName}.xlsx`);
    }

    /**
     * 解析 Excel 文件为 JSON,并对数据进行格式化处理
     * @param file 上传的文件
     * @param callback 数据处理回调
     * @returns 解析后的 JSON 数据
     */
    static async parseJson(file: File, callback: (row: ExcelRecord, index: number) => ExcelRecord) {
        try {
            if (!file) {
                throw new Error('请选择要上传的文件');
            }
            const json = (await XlsxTools.parse(file)) as ExcelRecord[];
            const formattedJson = json.map(callback);
            return formattedJson;
        } catch (error: any) {
            message.error(error.message || '文件解析失败');
        }
    }

    /**
     * 设置列宽
     * @param columnWidths 列宽数组
     * @param data 数据
     * @returns 列宽对象数组
     */
    private static setColumnWidths<T extends ExcelRecord>(columnWidths: number[], data: T[]) {
        return Object.keys(data[0]).map((_, i) => ({
            wch: columnWidths[i] || 50,
        }));
    }

    /**
     * 解析文件为 JSON
     * @param file 文件
     * @returns 解析后的 JSON 数据
     */
    private static parse(file: File) {
        return new Promise<ExcelRecord[]>((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = function (e: ProgressEvent<FileReader>) {
                try {
                    const data = new Uint8Array(e.target!.result as ArrayBuffer);
                    const workbook = XLSX.read(data, { type: 'array' });
                    const worksheet = workbook.Sheets[workbook.SheetNames[0]];
                    const json = XLSX.utils.sheet_to_json<ExcelRecord>(worksheet, { header: 2 });
                    resolve(json);
                } catch (err) {
                    reject(err);
                }
            };
            reader.onerror = () => {
                reject(new Error('文件读取失败'));
            };
            reader.readAsArrayBuffer(file);
        });
    }

    /**
     * 合并单元格范围
     * @param data 数据
     * @param mergeKeys 需要合并的列键数组
     * @returns 合并范围数组
     */
    private static mergeRanges<T extends ExcelRecord>(data: T[], mergeKeys: string[]) {
        const mergeRanges: { s: { r: number; c: number }; e: { r: number; c: number } }[] = [];
        const keys = mergeKeys || Object.keys(data[0]);

        keys.forEach((columnName, i) => {
            let startIndex = 1;
            let endIndex = 1;

            for (let rowIndex = 1; rowIndex < data.length; rowIndex++) {
                const currentRow = data[rowIndex];
                const previousRow = data[rowIndex - 1];

                if (currentRow[columnName] === previousRow[columnName]) {
                    endIndex++;
                } else {
                    if (endIndex > startIndex) {
                        mergeRanges.push({
                            s: { r: startIndex, c: i },
                            e: { r: endIndex, c: i },
                        });
                    }
                    startIndex = endIndex = rowIndex + 1;
                }
            }

            if (endIndex > startIndex) {
                mergeRanges.push({
                    s: { r: startIndex, c: i },
                    e: { r: endIndex, c: i },
                });
            }
        });

        return mergeRanges;
    }
}

export default XlsxTools;

场景&示例

上边提到我们上传的是中文标头的Excel,需要处理成后端的英文字段,所以需要和后端以及运营人员基于模版出上传Excel约定到表头字段,前端那么需要做的就是根据中文和英文做一个映射,在解析阶段去处理下,还有就是比如excel中的一些值需要处理成后端提供的的枚举值 女-0 男-1等等 比如:

这是上传后xlsx解析出来的数据 image.png

表头映射:

export const importKeyMap = new Map([
    ['手机号', 'phone'],
    ['城市', 'city'],
    ['行业', 'industry'],
    ['学历', 'educationLevel'],
    ['婚姻状况', 'marital'],
    ['生育状况', 'child'],
    ['年龄', 'birthYear'],
    ['购买车型', 'purchasedCar'],
    ['意向车型', 'intendedCar'],
    ['持有车型', 'ownedCar'],
    ['性别', 'gender'],
]);

值需要处理后端需要的枚举值

export const genderMap = new Map([
    [GenderTypeEnum.UNKNOWN, '未知'],
    [GenderTypeEnum.MALE, '男'],
    [GenderTypeEnum.FEMALE, '女'],
]);

export const genderReverseMap = reverseMap(genderMap);

export function reverseMap<T, U>(map: Map<T, U>): Map<U, T> {
    const reversedMap = new Map<U, T>();
    for (const [key, value] of map.entries()) {
        reversedMap.set(value, key);
    }
    return reversedMap;
}

大概的基础数据,基于以上数据处理成后端需要的json格式并上传,我这边就直接贴代码了:

 /**
     * 上传excel & 解析 & map
     */
    const uploadExcelAndParseToJson = async () => {
        // 上传文件
        const files = (await uploadDialog({ isMultiple: false, accept: acceptEnum.EXCEL })) as FileList;
        const excelJson = await XlsxTools.parseJson(files[0], (row) => {
            const formattedRow: Record<string, string | number> = {};
            const keysToCheck = [...importKeyMap.keys()];
            const lack: ExcelRecord = {};
            keysToCheck.forEach((k) => {
                if (!row.hasOwnProperty(k)) {
                    lack[k] = '';
                }
            });
            for (const [key, value] of Object.entries({ ...row, ...lack })) {
                const mappedKey = importKeyMap.get(key.replace(/\s/g, ''));
                if (mappedKey) {
                    switch (mappedKey) {
                        case 'phone':
                            formattedRow[mappedKey] = value.toString().replace(/\t/g, '');
                            break;
                        case 'educationLevel':
                            formattedRow[mappedKey] = educationLevelReverseMap.get(value) || EducationLevelTypeEnum.UNKNOWN;
                            break;
                        case 'marital':
                            formattedRow[mappedKey] = maritalReverseMap.get(value) || MaritalTypeEnum.UNKNOWN;
                            break;
                        case 'child':
                            formattedRow[mappedKey] = childReverseMap.get(value) || ChildTypeEnum.UNKNOWN;
                            break;
                        case 'gender':
                            formattedRow[mappedKey] = genderReverseMap.get(value) || GenderTypeEnum.UNKNOWN;
                            break;
                        case 'birthYear':
                            formattedRow[mappedKey] = +value;
                            break;
                        default:
                            formattedRow[mappedKey] = value || '';
                            break;
                    }
                } else {
                    const errMsg = `${key} - 表头不对!`;
                    throw Error(errMsg);
                }
            }
            return formattedRow;
        });
        return excelJson;
    };

    const onBatchUpload = async () => {
        try {
            const json = (await uploadExcelAndParseToJson()) as API.CreateUserExtraDto[];
            if (json.some((item) => !item.phone)) {
                message.error('手机号为必填项!');
                return;
            }
            await UserExtraCreate({ list: json });
            message.success('上传成功!');
            actionRef.current?.reload();
        } catch (error) {}
    };

最后处理完的json:

image.png

总结

在本文中,我们展示了如何使用 xlsx 库和 Ant Design 实现 Excel 文件的上传、解析和导出功能。通过创建一个通用的 XlsxTools 工具类,我们可以简化 Excel 文件处理的复杂度,并提供易于维护和扩展的代码结构。

我们介绍了如何通过 exportByDownLoadLink 方法导出文件,如何通过 exportByArray 方法将数组数据导出为 Excel 文件,以及如何使用 parseJson 方法解析 Excel 文件并对数据进行格式化处理。最后,我们展示了一个具体的使用示例,演示了如何上传和解析 Excel 文件,并将其转换为所需的 JSON 数据格式。

通过这些步骤,你可以轻松地将 Excel 文件处理功能集成到你的 React 应用中,提升用户体验和数据管理的效率。如果你有更多的需求,比如支持更多的 Excel 特性或处理更复杂的数据转换,你可以在此基础上进一步扩展 XlsxTools 工具类。

如果觉得对你有点帮助,点赞收藏支持下吧 谢谢🐶!