Vue3中使用exceljs和file-saver实现Excel导出(含图片导出)完整方案

发布于:2025-03-12 ⋅ 阅读:(13) ⋅ 点赞:(0)

📝 问题背景

在Vue3项目中,当我们需要将包含图片的数据导出到Excel时,常用的sheetjs/xlsx库存在局限性:无法直接导出图片到单元格。本文将提供完整的解决方案,封装可直接复用的工具函数。


🎯 解决方案

技术选型

使用exceljs + file-saver组合:

  • exceljs:支持图片插入的Excel操作库
  • file-saver:前端文件保存工具

功能特性

✅ 支持多图片列导出
✅ 自动识别Base64和DataURL格式
✅ 精确控制图片位置
✅ 异常处理与性能优化


🛠 完整实现代码

步骤1:安装依赖

npm install exceljs file-saver

步骤2:创建工具文件

// utils/excelExport.js
import ExcelJS from 'exceljs';
import { saveAs } from 'file-saver';

/**
 *
 * @param headers 表头,[表头1,表头2,...]          必填
 * @param tableData 内容 [data1,data2,...]       必填
 * @param imageIndex 图片下标列 [0, 1, 2 , ...],     非必填
 * @returns {Promise<void>}
 */
export const exportToExcel = async (headers, tableData, imageIndex = []) => {
    const workbook = new ExcelJS.Workbook();
    const worksheet = workbook.addWorksheet('Sheet1');

    // 添加表头
    const headerRow = worksheet.addRow(headers);

    // 设置表头样式
    headerRow.eachCell((cell) => {
        cell.font = {
            bold: true,
            color: { argb: 'FFFFFFFF' },
            size: 12
        };
        cell.fill = {
            type: 'pattern',
            pattern: 'solid',
            fgColor: { argb: 'FF007BFF' }
        };
        cell.alignment = {
            horizontal: 'center',
            vertical: 'middle'
        };
    });

    // 设置列宽
    headers.forEach((_, colIndex) => {
        let width = 20; // 默认列宽
        if (imageIndex.includes(colIndex)) {
            width = 30; // 图片列更宽
        }
        worksheet.getColumn(colIndex + 1).width = width;
    });

    // 处理数据行(修改为异步)
    for (let rowIdx = 0; rowIdx < tableData.length; rowIdx++) {
        const row = tableData[rowIdx];
        const dataRow = worksheet.addRow(row);

        if (imageIndex.length > 0) {
            // 使用Promise.all处理多列图片
            await Promise.all(imageIndex.map(async (colIdx) => {
                const imgUrl = row[colIdx];
                if (!imgUrl) return;

                try {
                    const parsed = await parseImageData(imgUrl);
                    if (!parsed) return;

                    const imageId = workbook.addImage({
                        base64: parsed.base64,
                        extension: parsed.extension,
                    });

                    const colLetter = getExcelColumnLetter(colIdx);
                    const excelRow = rowIdx + 2;

                    worksheet.addImage(imageId, {
                        tl: { col: colIdx, row: excelRow - 1 },
                        br: { col: colIdx + 1, row: excelRow },
                        editAs: 'oneCell' // 更稳定的定位方式
                    });
                } catch (error) {
                    console.error(`图片处理失败(行${rowIdx + 1}${colIdx + 1}):`, error);
                }
            }));
        }
    }

    // 生成文件
    const buffer = await workbook.xlsx.writeBuffer();
    saveAs(new Blob([buffer]), `数据导出_${new Date().getTime()}.xlsx`);
};

// 列索引转Excel字母(如:0 -> A, 1 -> B)
const getExcelColumnLetter = (column) => {
    let letter = '';
    let col = column + 1;
    while (col > 0) {
        const remainder = (col - 1) % 26;
        letter = String.fromCharCode(65 + remainder) + letter;
        col = Math.floor((col - 1) / 26);
    }
    return letter;
};

// 新增:将图片URL转换为base64
const urlToBase64 = async (url) => {
    try {
        const response = await fetch(url);
        const blob = await response.blob();
        return new Promise((resolve) => {
            const reader = new FileReader();
            reader.onloadend = () => resolve(reader.result);
            reader.readAsDataURL(blob);
        });
    } catch (error) {
        console.error('图片转换失败:', error);
        return null;
    }
};

// 修改后的解析函数
const parseImageData = async (imgData) => {
    if (!imgData) return null;

    // 处理URL情况
    if (imgData.startsWith('http')) {
        const dataURL = await urlToBase64(imgData);
        if (!dataURL) return null;

        const [header, base64] = dataURL.split(';base64,');
        const extension = header.split('/')[1];
        return { base64, extension };
    }

    // 原有处理dataURL的逻辑
    if (imgData.startsWith('data:image')) {
        const [header, base64] = imgData.split(';base64,');
        if (!header || !base64) return null;
        const extension = header.split('/')[1];
        return { base64, extension };
    }

    return null;
};


📌 使用示例

Vue3组件调用

<template>
  <button @click="handleExport">导出Excel</button>
</template>

<script setup>
import { exportDataToExcel } from '@/utils/excelExport';

const headers = ['名称', '价格', '主图'];
const tableData = [
  ['商品A', 99.9, 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'], 
  ['商品B', 199.9, '']
];
const imageIndex = [2]; // 第三列为图片列

const handleExport = async () => {
  await exportDataToExcel(headers, tableData, imageIndex);
};
</script>

⚙️ 扩展配置

设置列宽

    headers.forEach((_, colIndex) => {
        let width = 20; // 默认列宽
        if (imageIndex.includes(colIndex)) {
            width = 30; // 图片列更宽
        }
        worksheet.getColumn(colIndex + 1).width = width;
    });

设置表头样式

headerRow.eachCell((cell) => {
        cell.font = {
            bold: true,
            color: { argb: 'FFFFFFFF' },
            size: 12
        };
        cell.fill = {
            type: 'pattern',
            pattern: 'solid',
            fgColor: { argb: 'FF007BFF' }
        };
        cell.alignment = {
            horizontal: 'center',
            vertical: 'middle'
        };
    });

📌 导出样式

在这里插入图片描述


⚠️ 注意事项

  1. 图片规范

    • 建议使用PNG格式
    • 单图片尺寸不超过500x500px
    • Base64字符串需完整
  2. 性能建议

    • 推荐数据量:<1000行
    • 单文件图片:<50张
    • 大文件建议分页导出
  3. 常见问题

    • 图片不显示:检查Base64格式是否正确
    • 位置偏移:调整tl/br定位参数
    • 内存溢出:分批次处理数据

📚 实现原理

  1. 坐标转换
    通过getExcelColumnLetter函数将数字索引转换为Excel字母坐标

  2. 图片处理

    • 自动剥离DataURL头信息
    • 支持纯Base64直接使用
  3. 定位系统

    // tl: 左上角定位(top-left)
    // br: 右下角定位(bottom-right)
    { 
      tl: { col: 0, row: 0 }, // A1单元格
      br: { col: 1, row: 1 }  // B2单元格
    }
    

💡 总结

本文提供的方案具有以下优势:

  1. 开箱即用:封装完整工具函数
  2. 灵活配置:支持多图片列、自定义样式
  3. 健壮性强:完善的异常处理机制

建议根据实际业务需求调整图片定位参数和样式配置,对于复杂报表建议结合后端生成方案。