【Vue】HutoolExcel匯出

emdzz發表於2024-03-07

最近寫Excel匯出功能,發現需求有點複雜,這裡整理一下思路和解決方案

一、需求背景:

老系統改造,功能和程式碼是現成的,預覽了一下內容:

第一個是有特定樣式,比如首行標題,以及紅色的列名稱

第二個,匯出多個Sheet頁

第三個,最後多一行放置匯出時間

二、技術選型 :

我非常喜歡用Hutool的工具處理,然後看了下最新的文件案例,推測是可以滿足上述需求的

http://hutool.cn/docs/#/poi/Excel生成-ExcelWriter

重點是關於如何多Sheet頁匯出的支援,Hutool這裡沒有細說,看看有沒有現成的案例

經過簡單測試發現是可行的

https://blog.csdn.net/ZLK1142/article/details/106531246/

  

三、落地實現:

1、前後互動問題:

 本來是打算使用前端匯出的,後端介面提供資料即可,但是前端匯出怎麼設定具體樣式並不熟悉,加上自定義樣式需求多,就放棄這個方案了

 使用後端匯出的基本辦法是使用get請求方式的介面,然後前端使用window.open()開啟新連結,這樣跳轉下載一個檔案

  - 這樣好處是不用編寫互動處理,使用者等待新頁面彈出下載提示即可

  - 但是請求引數,令牌資訊都要透過url攜帶,不安全的,也會暴露資訊

 再加上現有系統無法從url上獲取引數,所以改用axios請求實現

 axios請求實現的問題在於響應的處理,要在前端宣告特定的blob型別、重新封裝檔案內容、和下載事件處理

2、Hutool基於業務需求的封裝:

 之前寫的匯出就是匯出資料就行,這裡參考多sheet自己實現的一個邏輯

Hutool支援了用Map配置別名對映,為了更方便實現更符合業務邏輯方式的開發,可以自定義對映註解

package cn.anmte.wd.excel.annotation;

import java.lang.annotation.*;

/**
 * @description Excel 欄位資訊
 * @author OnCloud9
 * @date 2024/3/6 16:26
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExportAlias {
    String value();
}

透過註解的匯出實體類,可以得到對映資訊

    /**
     * @description 獲取匯出資料別名對映
     * @author OnCloud9
     * @date 2024/3/6 16:16
     * @params
     * @return
     */
    public static <ExportDTO> Map<String, String> getExportDataAlias(Class<ExportDTO> exportClass) {
        Map<String, String> aliasMap = new HashMap<>();
        Field[] declaredFields = exportClass.getDeclaredFields();
        for (Field field : declaredFields) {
            String name = field.getName();
            ExportAlias annotation = field.getAnnotation(ExportAlias.class);
            if (Objects.isNull(annotation)) continue;
            aliasMap.put(name, annotation.value());
        }
        return aliasMap;
    }

因為是多sheet,所以要宣告一個類封裝sheet資訊

package cn.anmte.wd.excel;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SheetInfo<ExportData> {
    private String sheetName;
    private String sheetTitle;
    /* 匯出實體的class物件 */
    private Class<ExportData> exportDataClass;
    /* 匯出的資料 */
    private List<ExportData> exportDataList;

    /* 別名對映過濾方法 預設不處理 */
    private Function<Map<String, String>, Map<String, String>> aliasFilter;

    /* 資料寫入完成後的操作 -> 預設執行內容 */
    private Consumer<WdImcExcelUtil.AfterWrite> awConsumer;
}

  

存在動態列名匯出的場景,這裡基於解析對映資訊的基礎上,追加了調整對映資訊的方法:

提供一個Function方法介面,投入解析好的對映Map,具體調整方法交給外部呼叫實現

private static Map<String, String> aliasConfig(ExcelWriter writer, SheetInfo<?> sheetInfo) {
        Map<String, String> aliasMap = getExportDataAlias(sheetInfo.getExportDataClass());
        Function<Map<String, String>, Map<String, String>> aliasFilter = sheetInfo.getAliasFilter();
        if (Objects.nonNull(aliasFilter)) aliasMap = aliasFilter.apply(aliasMap);
        writer.clearHeaderAlias();
        writer.setHeaderAlias(aliasMap);
        writer.setOnlyAlias(true);
        return aliasMap;
    }

  

WebServlet下載邏輯部分:

    private static void exportForDownload(HttpServletResponse response, ExcelWriter writer, String workBookName) {
        ServletOutputStream out = null;
        try {
            response.setContentType("application/vnd.ms-excel;charset=utf-8");
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(workBookName + ".xlsx", "UTF-8"));
            out = response.getOutputStream();
            writer.flush(out, true);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 關閉writer,釋放記憶體
            writer.close();
        }
        // 關閉輸出Servlet流
        IoUtil.close(out);
    }

  

匯出整裝的邏輯:

後置處理和上面的動態對映也是同一個邏輯,外部實現

因為我想把那些樣式處理放到這裡統一執行,邏輯層次是清晰的,易於維護

在下面匯出可以再補充其他下載方式,開發時間有限就寫到這個程度了

    /**
     * @description
     * @author Cloud9
     * @date 2024/3/6 17:02
     * @params
     * @return
     */
    public static void writeWdMultiSheetWorkBook(HttpServletResponse response, String workBookName, List<SheetInfo<?>> sheetInfoList) {
        if (CollectionUtils.isEmpty(sheetInfoList)) return;
        ExcelWriter writer = ExcelUtil.getWriter();
        /* 開啟多sheet頁支援方法 */
        writer.renameSheet(0, sheetInfoList.get(0).getSheetName());
        sheetInfoList.forEach(sheetInfo -> {
            /* sheet名稱設定 */
            writer.setSheet(sheetInfo.getSheetName());
            /* sheet別名對映設定 */
            Map<String, String> aliasMap = aliasConfig(writer, sheetInfo);
            /* 設定標頭內容 */
            if(StringUtils.isNotBlank(sheetInfo.getSheetTitle())) writer.merge(aliasMap.size() - 1, sheetInfo.getSheetTitle());
            /* 寫入資料 */
            writer.write(sheetInfo.getExportDataList(), true);
            /* 後置處理 */
            Consumer<AfterWrite> awConsumer = sheetInfo.getAwConsumer();
            if (Objects.nonNull(awConsumer)) awConsumer.accept(AfterWrite.builder().writer(writer).aliasMap(aliasMap).build());
        });
        exportForDownload(response, writer, workBookName);
    }

  

完整工具類程式碼(WdImcExcelUtil):

這裡樣式設定的程式碼沒完全寫好,可以提供參考

package cn.anmte.wd.excel;

import cn.anmte.wd.excel.annotation.ExportAlias;
import cn.hutool.core.io.IoUtil;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import cn.hutool.poi.excel.style.StyleUtil;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * @description
 *  https://www.hutool.cn/docs/#/poi/Excel%E7%94%9F%E6%88%90-ExcelWriter
 * @author OnCloud9
 * @date 2024/3/6 16:11
 */
public class WdImcExcelUtil {


    /**
     * @description 獲取當前時間線字尾
     * @author OnCloud9
     * @date 2024/3/6 17:13
     * @params
     * @return
     */
    public static String getCurrentTimeSuffix(String format) {
        if (StringUtils.isBlank(format)) format = "yyyyMMddHHmmss";
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
        return LocalDateTime.now().format(formatter);
    }


    /**
     * @description
     * @author OnCloud9
     * @date 2024/3/6 17:33
     * @params
     * @return
     */
    public static void writeWdSheetWorkBook(HttpServletResponse response, String workBookName, SheetInfo<?> sheetInfo) {
        ExcelWriter writer = ExcelUtil.getWriter();
        writer.renameSheet(0, sheetInfo.getSheetName());
        writer.setSheet(sheetInfo.getSheetName());
        /* sheet別名對映設定 */
        Map<String, String> aliasMap = aliasConfig(writer, sheetInfo);
        /* 設定標頭內容 */
        if(StringUtils.isNotBlank(sheetInfo.getSheetTitle())) writer.merge(aliasMap.size() - 1, sheetInfo.getSheetTitle());
        /* 寫入資料 */
        writer.write(sheetInfo.getExportDataList(), true);
        /* 後置處理 */
        Consumer<AfterWrite> awConsumer = sheetInfo.getAwConsumer();
        if (Objects.nonNull(awConsumer)) awConsumer.accept(AfterWrite.builder().writer(writer).aliasMap(aliasMap).build());
        exportForDownload(response, writer, workBookName);
    }

    /**
     * @description
     * @author OnCloud9
     * @date 2024/3/6 17:02
     * @params
     * @return
     */
    public static void writeWdMultiSheetWorkBook(HttpServletResponse response, String workBookName, List<SheetInfo<?>> sheetInfoList) {
        if (CollectionUtils.isEmpty(sheetInfoList)) return;
        ExcelWriter writer = ExcelUtil.getWriter();
        /* 開啟多sheet頁支援方法 */
        writer.renameSheet(0, sheetInfoList.get(0).getSheetName());
        sheetInfoList.forEach(sheetInfo -> {
            /* sheet名稱設定 */
            writer.setSheet(sheetInfo.getSheetName());
            /* sheet別名對映設定 */
            Map<String, String> aliasMap = aliasConfig(writer, sheetInfo);
            /* 設定標頭內容 */
            if(StringUtils.isNotBlank(sheetInfo.getSheetTitle())) writer.merge(aliasMap.size() - 1, sheetInfo.getSheetTitle());
            /* 寫入資料 */
            writer.write(sheetInfo.getExportDataList(), true);
            /* 後置處理 */
            Consumer<AfterWrite> awConsumer = sheetInfo.getAwConsumer();
            if (Objects.nonNull(awConsumer)) awConsumer.accept(AfterWrite.builder().writer(writer).aliasMap(aliasMap).build());
        });
        exportForDownload(response, writer, workBookName);
    }

    private static Map<String, String> aliasConfig(ExcelWriter writer, SheetInfo<?> sheetInfo) {
        Map<String, String> aliasMap = getExportDataAlias(sheetInfo.getExportDataClass());
        Function<Map<String, String>, Map<String, String>> aliasFilter = sheetInfo.getAliasFilter();
        if (Objects.nonNull(aliasFilter)) aliasMap = aliasFilter.apply(aliasMap);
        writer.clearHeaderAlias();
        writer.setHeaderAlias(aliasMap);
        writer.setOnlyAlias(true);
        return aliasMap;
    }

    private static void exportForDownload(HttpServletResponse response, ExcelWriter writer, String workBookName) {
        ServletOutputStream out = null;
        try {
            response.setContentType("application/vnd.ms-excel;charset=utf-8");
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(workBookName + ".xlsx", "UTF-8"));
            out = response.getOutputStream();
            writer.flush(out, true);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 關閉writer,釋放記憶體
            writer.close();
        }
        // 關閉輸出Servlet流
        IoUtil.close(out);
    }

    /**
     * @description 獲取匯出資料別名對映
     * @author OnCloud9
     * @date 2024/3/6 16:16
     * @params
     * @return
     */
    public static <ExportDTO> Map<String, String> getExportDataAlias(ExportDTO dto) {
        Map<String, String> aliasMap = new HashMap<>();
        Class<?> exportClass = dto.getClass();
        Field[] declaredFields = exportClass.getDeclaredFields();
        for (Field field : declaredFields) {
            String name = field.getName();
            ExportAlias annotation = field.getAnnotation(ExportAlias.class);
            if (Objects.isNull(annotation)) continue;
            aliasMap.put(name, annotation.value());
        }
        return aliasMap;
    }

    /**
     * @description 獲取匯出資料別名對映
     * @author OnCloud9
     * @date 2024/3/6 16:16
     * @params
     * @return
     */
    public static <ExportDTO> Map<String, String> getExportDataAlias(Class<ExportDTO> exportClass) {
        Map<String, String> aliasMap = new HashMap<>();
        Field[] declaredFields = exportClass.getDeclaredFields();
        for (Field field : declaredFields) {
            String name = field.getName();
            ExportAlias annotation = field.getAnnotation(ExportAlias.class);
            if (Objects.isNull(annotation)) continue;
            aliasMap.put(name, annotation.value());
        }
        return aliasMap;
    }

    /**
     * @description 頭部樣式設定
     * @author OnCloud9
     * @date 2024/3/6 16:08
     * @params
     * @return
     */
    public static void headerStyleSetting(ExcelWriter excelWriter) {
        CellStyle cellStyle = StyleUtil.createCellStyle(excelWriter.getWorkbook());
        Sheet sheet = excelWriter.getSheet();
        Row row = sheet.getRow(0);

        /* 設定單元行高度為50磅  */
        row.setHeight((short) 1000);

        /* 建立頭部樣式的自定義字型 */
        Font font = excelWriter.createFont();
        font.setFontName("Arial");
        font.setBold(true);
        font.setFontHeightInPoints((short) 24);

        /* 設定預設的背景色 */
        cellStyle.setFont(font);
        cellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
        cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        cellStyle.setAlignment(HorizontalAlignment.CENTER);  // 設定水平居中
        cellStyle.setVerticalAlignment(VerticalAlignment.CENTER); // 設定垂直居中

        /* 放置樣式 */
        Cell cell = row.getCell(0);
        cell.setCellStyle(cellStyle);
    }

    /**
     * @description 列名樣式設定
     * @author OnCloud9
     * @date 2024/3/6 16:08
     * @params
     * @return
     */
    public static void columnNameStyleSetting(ExcelWriter excelWriter, int colSize) {
        CellStyle cellStyle = StyleUtil.createCellStyle(excelWriter.getWorkbook());
        Sheet sheet = excelWriter.getSheet();
        Row row = sheet.getRow(1);

        /* 建立頭部樣式的自定義字型 */
        Font font = excelWriter.createFont();
        font.setFontName("Arial");
        font.setBold(true);
        font.setColor(Font.COLOR_RED);
        font.setFontHeightInPoints((short) 10);

        /* 設定樣式 */
        cellStyle.setFont(font);
        cellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
        cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        /* 邊框樣式 */
        // 建立邊框物件,並設定邊框樣式
        BorderStyle borderStyle = BorderStyle.THIN; // 細邊框
        short blackIndex = IndexedColors.BLACK.getIndex();
        cellStyle.setBorderTop(borderStyle); // 設定上邊框
        cellStyle.setTopBorderColor(blackIndex); // 設定邊框顏色為黑色
        cellStyle.setBorderBottom(borderStyle); // 設定下邊框
        cellStyle.setBottomBorderColor(blackIndex);
        cellStyle.setBorderLeft(borderStyle);  // 設定左邊框
        cellStyle.setLeftBorderColor(blackIndex);
        cellStyle.setBorderRight(borderStyle); // 設定右邊框
        cellStyle.setRightBorderColor(blackIndex);

        for (int i = 0; i < colSize; i++) {
            Cell cell = row.getCell(i);
            cell.setCellStyle(cellStyle);
        }
    }

    /**
     * @description
     * @author OnCloud9
     * @date 2024/3/7 17:37
     * @params
     * @return
     */
    public static void timelineMark(ExcelWriter excelWriter, int rowIdx, int colIdx) {
        CellStyle cellStyle = StyleUtil.createCellStyle(excelWriter.getWorkbook());
        String currentTime = getCurrentTimeSuffix("yyyy-MM-dd HH:mm:ss");
        excelWriter.setCurrentRow(rowIdx);
        excelWriter.merge(colIdx, "時間:" + currentTime);
        Cell cell = excelWriter.getOrCreateCell(rowIdx, 0);

        /* 設定右居中 */
        cellStyle.setAlignment(HorizontalAlignment.RIGHT);  // 設定右居中
        cellStyle.setVerticalAlignment(VerticalAlignment.CENTER); // 設定垂直居中
        cellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
        cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        cell.setCellStyle(cellStyle);
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public static class AfterWrite {
        private ExcelWriter writer;
        private Map<String, String> aliasMap;
    }

}

3、前端JS部分:

封裝好axios處理

import { CUSTOMAPIURl } from '@/utils/define'
import axios from 'axios'


/**
 * @param apiPath 請求路徑
 * @param postData 請求引數
 * @param token 令牌
 * @param fileName 檔名
 * @param whenDone 完成時回撥
 * @param whenErr 異常時回撥
 */
export function requestExcelExport({ apiPath, postData, token, fileName, whenDone, whenErr }) {
  axios({
    method: 'post',
    url: CUSTOMAPIURl + apiPath,
    data: postData,
    responseType: 'blob',
    headers: {
      'Content-Type': 'application/json', // 示例的 header,你可以根據需要新增更多
      'Authorization': token// 示例的授權 header
    }
  }).then(function (response) {
    // 建立一個 blob URL
    const blobUrl = window.URL.createObjectURL(new Blob([response.data]))
    const link = document.createElement('a')
    link.href = blobUrl;
    link.setAttribute('download', fileName); // 設定下載檔案的名稱
    document.body.appendChild(link)
    // 觸發點選事件來下載檔案
    link.click();
    // 清理
    window.URL.revokeObjectURL(blobUrl);
    document.body.removeChild(link);
    whenDone()
  }).catch(function (error) {
    // 請求失敗後的處理
    console.error('Error downloading Excel file:', error);
    whenErr()
  })
}

  

方法使用:

四、實現效果:

這裡還沒處理對映順序,看起來有點亂,等後面我再追加補充吧....

相關文章