Spring Boot + Apache POI 实现 Excel 导出:BOM物料清单生成器(支持中文文件名、样式美化、数据合并)

news2025/1/20 1:27:47

目录

引言

Apache POI操作Excel的实用技巧

1.合并单元格操作

2.设置单元格样式

1. 创建样式对象

2. 设置边框

3. 设置底色

4. 设置对齐方式

5. 设置字体样式

6.设置自动换行

7. 应用样式到单元格

3. 定位和操作指定单元格

4.实现标签-值的形式

5.列宽设置

1. 设置单个列宽

2. 批量设置多列宽度

6.数据格式化

1. 设置数字格式

2. 设置日期格式

代码展示

1.POM依赖

2.实体类Mode

3.Controller层

4.Service层

获取源码



引言

在最近的MES系统开发中,我们需要导出BOM物料清单,并且客户对样式有较高要求。这就涉及到对POI库样式的精细调整,包括设置表格边框合并单元格设置单元格底色等常见操作。我通过实现一种模板,使得样式设计既美观又实用,并可以根据这个模板创建其他自定义格式。这一模板的主要功能包括:设置Excel表格的边框样式、添加背景色、合并单元格以及采用标签-值的展示形式(如“订单编号:BH000001”)。接下来,我将分享如何通过这种模板实现灵活的Excel导出功能,满足不同业务需求。

如下图是POM清单的一个实现模版:

下面让我们来先学习怎么通过POI库来实现表格边框,合并单元格,设置单元格底色,以及采用标签-值的展示形式这一系列操作!

Apache POI操作Excel的实用技巧

1.合并单元格操作

这个代码片段创建了一个Excel文件,生成了一个名为 "BOM物料清单" 的工作表,在第2行合并了从第1列到第10列的单元格(从第A列到第J列)最后,代码将生成的Excel文件转换为字节数组并返回。

// 使用 try-with-resources 语句,确保 Workbook 在使用完毕后会自动关闭
try (Workbook workbook = new XSSFWorkbook()) {
    // 创建一个名为 "BOM物料清单" 的工作表
    Sheet sheet = workbook.createSheet("BOM物料清单");

    // 创建第2行,行号从0开始,因此创建第2行的行号是1
    Row titleRow = sheet.createRow(1);

    // 合并第2行的0列到9列的单元格,CellRangeAddress的参数:起始行、结束行、起始列、结束列
    sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, 9));

    // 在合并后的区域中设置单元格内容
    Cell titleCell = titleRow.createCell(0); // 在第1列创建单元格
    titleCell.setCellValue("BOM物料清单"); // 设置显示内容,例如 "BOM物料清单"

    // 创建输出流,用于将工作簿内容写入输出流
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    
    // 将工作簿写入到输出流中
    workbook.write(outputStream);
    
    // 返回字节数组,outputStream.toByteArray() 返回Excel文件的二进制数据
    return outputStream.toByteArray();
}

效果如下:

2.设置单元格样式

// 1. 创建样式对象
CellStyle style = workbook.createCellStyle();

// 2. 设置边框
style.setBorderTop(BorderStyle.THIN);    // 上边框
style.setBorderBottom(BorderStyle.THIN); // 下边框
style.setBorderLeft(BorderStyle.THIN);   // 左边框
style.setBorderRight(BorderStyle.THIN);  // 右边框

// 3. 设置底色
style.setFillForegroundColor(IndexedColors.LIGHT_GREEN.getIndex()); // 设置颜色
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);           // 设置填充模式

// 4. 设置对齐方式
style.setAlignment(HorizontalAlignment.CENTER);      // 水平居中
style.setVerticalAlignment(VerticalAlignment.CENTER); // 垂直居中

// 5. 设置字体样式
Font font = workbook.createFont();
font.setBold(true); // 设置加粗
font.setColor(IndexedColors.RED.getIndex()); // 设置字体颜色为红色
font.setFontHeightInPoints((short) 12); // 设置字体大小
style.setFont(font); // 将字体应用到样式中

// 6. 设置自动换行
style.setWrapText(true); // 启用自动换行

// 7. 应用样式到单元格
cell.setCellStyle(style);

代码效果:

  • 边框:单元格会有细线的上下左右边框。
  • 底色:单元格背景为浅绿色。
  • 对齐方式:单元格内容会居中对齐(水平和垂直)。
  • 字体样式:字体加粗,颜色为红色,字体大小为12磅。
  • 自动换行:如果单元格内容过长,内容会自动换行。

如下是对每一个小功能的详细解释:

1. 创建样式对象

createCellStyle(): 这个方法是创建一个新的 CellStyle 对象,所有的样式设置都会在这个对象上进行。CellStyle 可以定义单元格的外观,如边框、对齐方式、字体、填充颜色等。

2. 设置边框

setBorderTop:设置单元格的上边框样式为 THIN(细线)。BorderStyle 枚举提供了几种边框样式:THIN(细线)、THICK(粗线)、DOTTED(点线)、DASHED(虚线)等。

setBorderBottom:设置单元格的下边框样式。

setBorderLeft:设置单元格的左边框样式。

setBorderRight:设置单元格的右边框样式。

3. 设置底色

setFillForegroundColor:设置单元格的前景色(填充颜色)。在这里,IndexedColors.LIGHT_GREEN.getIndex() 获取了 IndexedColors 枚举中的 LIGHT_GREEN(浅绿色)的颜色索引值,getIndex() 方法返回该颜色的数字标识。IndexedColors 枚举包括常见颜色,如 YELLOWREDBLUE 等。

setFillPattern(FillPatternType.SOLID_FOREGROUND):设置单元格的填充模式为 SOLID_FOREGROUND,表示填充整个单元格背景色。FillPatternType 还可以选择其他模式,如 NO_FILL(不填充)SOLID_FOREGROUND(完全填充)等。

颜色解释:

LIGHT_GREENIndexedColors 中的一个颜色常量,代表一种浅绿色。可以将单元格的背景色设置为浅绿色,增加表格的视觉效果,使其更具可读性和美观。

4. 设置对齐方式

setAlignment(HorizontalAlignment.CENTER):设置单元格内容在水平方向上的对齐方式。HorizontalAlignment.CENTER 表示水平居中对齐。其他选项包括 LEFT(左对齐)和 RIGHT(右对齐)。

setVerticalAlignment(VerticalAlignment.CENTER):设置单元格内容在垂直方向上的对齐方式。VerticalAlignment.CENTER 表示垂直居中对齐。其他选项包括 TOP(顶部对齐)和 BOTTOM(底部对齐)。

5. 设置字体样式

createFont():通过 workbook.createFont() 创建一个新的 Font 对象,用于设置字体样式。

setBold(true):设置字体加粗。

setColor(IndexedColors.RED.getIndex()):设置字体颜色为红色。IndexedColors.RED.getIndex() 获取了 IndexedColorsRED(红色)的颜色索引。除了 REDIndexedColors 还包含多种颜色,如 BLACKBLUEGREEN 等。

setFontHeightInPoints((short) 12):设置字体的大小为12磅(points)。你可以根据需求调整字体大小。

setFont(font):将字体样式应用到 CellStyle 中,使得字体的加粗、颜色和大小在单元格中生效。

6.设置自动换行

setWrapText(true):启用自动换行功能。当单元格内容过长时,文本会自动换行,以避免内容超出单元格边界。此设置特别有用,尤其是在表格中包含多行文字时。

7. 应用样式到单元格

setCellStyle(style):将之前定义的 style 应用到目标单元格 cell 上。所有在 style 中设置的样式(如边框、底色、字体、对齐方式等)都会在该单元格中生效。

3. 定位和操作指定单元格

通过行列号定位

行号和列号的下标都是从0开始,比如第一行的下标是0,第一列的下标是0

Row row = sheet.createRow(3);  // 注意:行号从0开始,3代表第四行
Cell cell = row.createCell(0); // 列号也从0开始,0代表第一列
cell.setCellValue("单元格内容");
  • sheet.createRow(3):这将创建或返回Excel表格中的第4行(因为行号从0开始,3表示第4行)。
  • row.createCell(0):这将创建或返回第4行的第1列单元格(列号从0开始,0表示第一列)。
  • cell.setCellValue("单元格内容"):为该单元格设置值为 "单元格内容"。

4.实现标签-值的形式

下面这段代码实现了在 Excel 表格中创建标签-值的形式,即每行包含一个标签单元格和一个值单元格。通常这种格式用于展示诸如 "订单编号"、"产品编号" 等信息,并且标签和对应的值是相邻的单元格。以下是代码的详细解释:

private void createLabelValuePair(Row row, int startCol, 
                                  String label, String value, 
                                  CellStyle labelStyle, CellStyle valueStyle) {
    // 创建标签单元格
    Cell labelCell = row.createCell(startCol);
    labelCell.setCellValue(label);
    labelCell.setCellStyle(labelStyle);  // 标签使用一种样式(如带底色)

    // 创建值单元格
    Cell valueCell = row.createCell(startCol + 1);
    valueCell.setCellValue(value);
    valueCell.setCellStyle(valueStyle);  // 值使用另一种样式(如不带底色)
}

参数说明:

  • Row row:表示当前行对象。在该行上创建标签和值的单元格。
  • int startCol:表示开始列的列号。标签单元格的列号是从 startCol 开始,值单元格紧随其后,列号为 startCol + 1
  • String label:标签文本,例如 "订单编号"、"产品编号" 等。
  • String value:标签对应的值,例如 "BH00000002"、"CP00000002" 等。
  • CellStyle labelStyle:标签单元格的样式,可以设置字体、对齐方式、背景色等。
  • CellStyle valueStyle:值单元格的样式,用于设置不同于标签的样式。

功能:

  • 该方法用于创建一对标签和值,两个单元格位于同一行(row)。标签单元格位于 startCol 列,值单元格位于 startCol + 1 列。
  • 这两个单元格分别应用不同的样式(labelStylevalueStyle),从而使标签和值的显示效果有所区别。标签单元格可能使用不同的字体、颜色、背景等样式,而值单元格则可能有不同的格式。

使用示例:

Row row = sheet.createRow(3); // 注意:行号从0开始,3代表第四行
createLabelValuePair(row, 0, "订单编号", "BH00000002", headerStyle, valueStyle);
createLabelValuePair(row, 2, "产品编号", "CP00000002", headerStyle, valueStyle);

5.列宽设置

1. 设置单个列宽

代码通过 sheet.setColumnWidth() 方法来调整列宽。下面是详细的解释:

sheet.setColumnWidth(columnIndex, 15 * 256); // 15个字符宽度

解释:

  • sheet.setColumnWidth(columnIndex, 15 * 256):该方法设置 Excel 中指定列的宽度。
    • columnIndex:表示列的索引,Excel 中列的索引从 0 开始。例如,0 表示 A 列,1 表示 B 列,以此类推。
    • 15 * 256:列宽的单位是 "字符宽度",但在 POI 中,单位是字符宽度的 1/256,因此需要乘以 256
      • 15 表示列宽为 15 个字符宽度(即单元格中可以容纳15个字符长度)。
      • 256 是 POI 中列宽的缩放因子,表示字符宽度单位是 1/256 的一个字符宽度。所以 15 * 256 表示列宽为 15 个字符的宽度。

在这个例子中,sheet.setColumnWidth(columnIndex, 15 * 256) 将设置列的宽度为15个字符的宽度,columnIndex 列号是动态指定的。

2. 批量设置多列宽度

private void setColumnWidths(Sheet sheet) {
    int[] widths = {15, 15, 15, 12, 12, 12, 15, 12, 15, 12};
    for (int i = 0; i < widths.length; i++) {
        sheet.setColumnWidth(i, widths[i] * 256);
    }
}

解释:

  • int[] widths = {15, 15, 15, 12, 12, 12, 15, 12, 15, 12};

    • 这是一个整型数组 widths,它存储了每一列的宽度设置值。数组中的每个值代表对应列的宽度(单位是字符的数量)。
    • 15, 15, 15 等表示前面列宽设置为 15 个字符,12, 12, 12 等表示后面列宽设置为 12 个字符。
  • for (int i = 0; i < widths.length; i++) { ... }

    • 这是一个循环,用于遍历 widths 数组中的每个元素。
    • widths.length 返回数组的长度(此处为 10),所以循环会执行 10 次,每次处理一个列的宽度。
  • sheet.setColumnWidth(i, widths[i] * 256);

    • i 是列索引,因此 i09(共10列)。
    • widths[i] 获取数组中当前索引位置的宽度值,将这个值乘以 256 以得到正确的列宽单位(字符宽度的 1/256)。
    • sheet.setColumnWidth(i, widths[i] * 256) 将依次设置每一列的宽度,确保列宽符合指定的字符数。

6.数据格式化

这段代码展示了如何在 Apache POI 中设置 Excel 单元格的数据格式,具体包括数字格式和日期格式的设置。以下是对每一部分的详细解释:

1. 设置数字格式

CellStyle numberStyle = workbook.createCellStyle();
DataFormat format = workbook.createDataFormat();
numberStyle.setDataFormat(format.getFormat("#,##0.00"));

解释:

  • CellStyle numberStyle = workbook.createCellStyle();

    • 这行代码创建一个新的 CellStyle 对象(numberStyle),该对象用于设置单元格的样式。
    • CellStyle 用于控制单元格的外观,比如字体、边框、对齐方式以及数据格式等。
  • DataFormat format = workbook.createDataFormat();

    • createDataFormat()Workbook 类的一个方法,用于创建一个 DataFormat 对象。DataFormat 类用于定义单元格的数据格式(如日期、数字、货币等)。
    • format 对象将用于设置不同类型的格式,允许将格式应用于 CellStyle 中。
  • numberStyle.setDataFormat(format.getFormat("#,##0.00"));

    • setDataFormat() 方法用于为 CellStyle 设置数据格式。
    • format.getFormat("#,##0.00") 使用 DataFormat 对象来定义具体的数字格式。这段格式表示:
      • #,##0.00:这是数字的格式模式,表示数字应该使用千位分隔符(,),并且保留两位小数(.00)。例如,数字 1234567.89 会显示为 1,234,567.89
      • # 是占位符,表示数字的每个位置(但如果没有值,它就不显示)。
      • 0 表示即使数字为零,也会显示该位置的零。
      • .00 表示保留两位小数,即使实际数据没有小数部分,也会显示为 0.00

2. 设置日期格式

CellStyle dateStyle = workbook.createCellStyle();
dateStyle.setDataFormat(format.getFormat("yyyy/mm/dd"));

解释:

  • CellStyle dateStyle = workbook.createCellStyle();

    • 这行代码创建一个新的 CellStyle 对象(dateStyle),用于设置日期单元格的格式。
    • 这个 CellStyle 对象将应用于日期格式的单元格。
  • dateStyle.setDataFormat(format.getFormat("yyyy/mm/dd"));

    • setDataFormat() 方法为 dateStyle 设置数据格式。
    • format.getFormat("yyyy/mm/dd") 使用 DataFormat 对象来设置日期的格式。具体格式为:
      • yyyy/mm/dd:这是日期的格式模式,表示日期应该显示为年-月-日的格式。
        • yyyy 表示四位年份(例如 2025)。
        • mm 表示两位月份(例如 01 表示一月,12 表示十二月)。
        • dd 表示两位日期(例如 01 表示第一天,31 表示最后一天)。
      • 例如,日期 2025/01/17 会显示为 2025/01/17

总结:

这段代码展示了如何为 Excel 单元格设置数字和日期格式,具体包括:

  1. 数字格式:将单元格内容设置为数字格式,使用千位分隔符并保留两位小数(例如:1,234,567.89)。
  2. 日期格式:将单元格内容设置为日期格式,显示为年-月-日(例如:2025/01/17)。

代码展示

1.POM依赖

POM依赖需要引入org.apache.poi

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>5.2.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>5.2.3</version>
        </dependency>

2.实体类Mode

BOM清单标题头

public class BOMHeader {
    private String orderNumber;        // 订单编号
    private String productNumber;      // 产品编号
    private String productName;        // 产品名称
    private String specification;      // 规格型号
    private Integer orderQuantity;     // 订单数量
    private String planStartDate;      // 计划投产日期
    private Integer productionCycle;   // 计划生产周期
    private String planEndDate;        // 计划截止日期
    private String deliveryDate;       // 订单交期
    private Double totalCost;          // 合计成本

    public BOMHeader(String orderNumber, String productNumber, String productName, String specification, Integer orderQuantity, String planStartDate, Integer productionCycle, String planEndDate, String deliveryDate, Double totalCost) {
        this.orderNumber = orderNumber;
        this.productNumber = productNumber;
        this.productName = productName;
        this.specification = specification;
        this.orderQuantity = orderQuantity;
        this.planStartDate = planStartDate;
        this.productionCycle = productionCycle;
        this.planEndDate = planEndDate;
        this.deliveryDate = deliveryDate;
        this.totalCost = totalCost;
    }

    public BOMHeader() {
    }

    public String getOrderNumber() {
        return orderNumber;
    }

    public void setOrderNumber(String orderNumber) {
        this.orderNumber = orderNumber;
    }

    public String getProductNumber() {
        return productNumber;
    }

    public void setProductNumber(String productNumber) {
        this.productNumber = productNumber;
    }

    public String getProductName() {
        return productName;
    }

    public void setProductName(String productName) {
        this.productName = productName;
    }

    public String getSpecification() {
        return specification;
    }

    public void setSpecification(String specification) {
        this.specification = specification;
    }

    public Integer getOrderQuantity() {
        return orderQuantity;
    }

    public void setOrderQuantity(Integer orderQuantity) {
        this.orderQuantity = orderQuantity;
    }

    public String getPlanStartDate() {
        return planStartDate;
    }

    public void setPlanStartDate(String planStartDate) {
        this.planStartDate = planStartDate;
    }

    public Integer getProductionCycle() {
        return productionCycle;
    }

    public void setProductionCycle(Integer productionCycle) {
        this.productionCycle = productionCycle;
    }

    public String getPlanEndDate() {
        return planEndDate;
    }

    public void setPlanEndDate(String planEndDate) {
        this.planEndDate = planEndDate;
    }

    public String getDeliveryDate() {
        return deliveryDate;
    }

    public void setDeliveryDate(String deliveryDate) {
        this.deliveryDate = deliveryDate;
    }

    public Double getTotalCost() {
        return totalCost;
    }

    public void setTotalCost(Double totalCost) {
        this.totalCost = totalCost;
    }
}

BOM清单列表

public class BOMItem {
    private String materialCode;    // 料号
    private String name;           // 品名
    private String modelNumber;    // 型号
    private String unit;           // 计量单位
    private Integer quantity;      // 用量
    private String manufacturer;   // 厂家/品牌
    private String channel;        // 途径/渠道
    private Double unitPrice;      // 单价
    private Double total;          // 合计
    private String remarks;        // 备注

    public BOMItem(String materialCode, String name, String modelNumber, String unit, Integer quantity, String manufacturer, String channel, Double unitPrice, Double total, String remarks) {
        this.materialCode = materialCode;
        this.name = name;
        this.modelNumber = modelNumber;
        this.unit = unit;
        this.quantity = quantity;
        this.manufacturer = manufacturer;
        this.channel = channel;
        this.unitPrice = unitPrice;
        this.total = total;
        this.remarks = remarks;
    }

    public BOMItem() {
    }

    public String getMaterialCode() {
        return materialCode;
    }

    public void setMaterialCode(String materialCode) {
        this.materialCode = materialCode;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getModelNumber() {
        return modelNumber;
    }

    public void setModelNumber(String modelNumber) {
        this.modelNumber = modelNumber;
    }

    public String getUnit() {
        return unit;
    }

    public void setUnit(String unit) {
        this.unit = unit;
    }

    public Integer getQuantity() {
        return quantity;
    }

    public void setQuantity(Integer quantity) {
        this.quantity = quantity;
    }

    public String getManufacturer() {
        return manufacturer;
    }

    public void setManufacturer(String manufacturer) {
        this.manufacturer = manufacturer;
    }

    public String getChannel() {
        return channel;
    }

    public void setChannel(String channel) {
        this.channel = channel;
    }

    public Double getUnitPrice() {
        return unitPrice;
    }

    public void setUnitPrice(Double unitPrice) {
        this.unitPrice = unitPrice;
    }

    public Double getTotal() {
        return total;
    }

    public void setTotal(Double total) {
        this.total = total;
    }

    public String getRemarks() {
        return remarks;
    }

    public void setRemarks(String remarks) {
        this.remarks = remarks;
    }
}

3.Controller层

import com.e.toexcel.model.BOMHeader;
import com.e.toexcel.model.BOMItem;
import com.e.toexcel.service.BOMExcelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/bom")
public class BOMController {
    @Autowired
    private BOMExcelService bomExcelService;

    @GetMapping("/export")
    public ResponseEntity<byte[]> exportBOM() {
        try {
            // 创建示例数据
            BOMHeader header = createSampleHeader();
            List<BOMItem> items = createSampleItems();

            // 生成Excel文件
            byte[] excelContent = bomExcelService.generateBOMExcel(header, items);

            // 设置响应头
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);

            // 使用 URLEncoder 对文件名进行编码
            String filename = URLEncoder.encode("BOM物料清单.xlsx", StandardCharsets.UTF_8.name());
            // 替换空格编码
            filename = filename.replaceAll("\\+", "%20");
            // 设置 Content-Disposition 头
            headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + filename);

            return ResponseEntity.ok()
                    .headers(headers)
                    .body(excelContent);

        } catch (Exception e) {
            e.printStackTrace(); // 添加日志输出以便调试
            return ResponseEntity.internalServerError().build();
        }
    }

    private BOMHeader createSampleHeader() {
        return new BOMHeader(
                "BH00000002",
                "CP00000002",
                "自动包装机",
                "CS5506",
                80000,
                "2024/3/15",
                25,
                "2024/4/10",
                "2024/4/20",
                98560000.00
        );
    }

    private List<BOMItem> createSampleItems() {
        List<BOMItem> items = new ArrayList<>();
        // 添加所有示例数据
        items.add(createBOMItem("WL000101", "传送带", "ZJ000101", "个", 40000, "ABC品牌", "采购链接: yyyy", 120.00));
        items.add(createBOMItem("WL000102", "电机", "ZJ000102", "个", 80000, "DEF品牌", "自制生产", 0.00));
        items.add(createBOMItem("WL000103", "控制器", "ZJ000103", "个", 40000, "GHI品牌", "采购链接: yyyy", 350.00));
        items.add(createBOMItem("WL000104", "感应器", "ZJ000104", "个", 160000, "JKL品牌", "采购链接: yyyy", 80.00));
        items.add(createBOMItem("WL000105", "支架", "ZJ000105", "个", 80000, "MNO品牌", "自制生产", 0.00));
        items.add(createBOMItem("WL000106", "螺丝套件", "ZJ000106", "个", 320000, "PQR品牌", "采购链接: yyyy", 25.00));
        items.add(createBOMItem("WL000107", "线缆", "ZJ000107", "个", 240000, "STU品牌", "采购链接: yyyy", 45.00));
        items.add(createBOMItem("WL000108", "外壳", "ZJ000108", "个", 80000, "VWX品牌", "自制生产", 0.00));
        items.add(createBOMItem("WL000109", "显示屏", "ZJ000109", "个", 80000, "YZA品牌", "采购链接: yyyy", 280.00));
        items.add(createBOMItem("WL000110", "按钮组", "ZJ000110", "个", 160000, "BCD品牌", "采购链接: yyyy", 95.00));
        items.add(createBOMItem("WL000111", "密封圈", "ZJ000111", "个", 240000, "EFG品牌", "采购链接: yyyy", 35.00));
        items.add(createBOMItem("WL000112", "铭牌", "ZJ000112", "个", 80000, "HIJ品牌", "采购链接: yyyy", 15.00));
        items.add(createBOMItem("WL000113", "包装材料", "ZJ000113", "个", 80000, "KLM品牌", "采购链接: yyyy", 12.00));
        return items;
    }

    private BOMItem createBOMItem(String code, String name, String model, String unit, 
                                 int quantity, String manufacturer, String channel, double price) {
        double total = channel.equals("自制生产") ? 0.00 : price * quantity;
        return new BOMItem(
                code,
                name,
                model,
                unit,
                quantity,
                manufacturer,
                channel,
                price,
                total,
                ""  // remarks
        );
    }
} 

4.Service层

package com.e.toexcel.service;

import com.e.toexcel.model.BOMHeader;
import com.e.toexcel.model.BOMItem;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;

@Service
public class BOMExcelService {
    private CellStyle valueStyle;  // 添加成员变量

    private void createTitle(Sheet sheet, CellStyle style) {
        Row titleRow = sheet.createRow(1);
        
        // 为所有要合并的单元格创建样式
        for (int i = 0; i < 10; i++) {
            Cell cell = titleRow.createCell(i);
            cell.setCellStyle(style);
            // 只在第一个单元格设置值
            if (i == 0) {
                cell.setCellValue("BOM物料清单");
            }
        }
        
        // 合并单元格
        sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, 9));
    }

    private void createBasicInfo(Sheet sheet, BOMHeader header, CellStyle style) {
        // 第一行基本信息
        Row row1 = sheet.createRow(3);
        createHeaderCell(row1, 0, "订单编号", header.getOrderNumber(), style);
        createHeaderCell(row1, 2, "产品编号", header.getProductNumber(), style);
        createHeaderCell(row1, 4, "产品名称", header.getProductName(), style);
        createHeaderCell(row1, 6, "规格型号", header.getSpecification(), style);
        createHeaderCell(row1, 8, "订单数量", String.valueOf(header.getOrderQuantity()), style);

        // 第二行基本信息
        Row row2 = sheet.createRow(4);
        createHeaderCell(row2, 0, "计划投产日期", header.getPlanStartDate(), style);
        createHeaderCell(row2, 2, "计划生产周期", String.valueOf(header.getProductionCycle()), style);
        createHeaderCell(row2, 4, "计划截止日期", header.getPlanEndDate(), style);
        createHeaderCell(row2, 6, "订单交期", header.getDeliveryDate(), style);
        createHeaderCell(row2, 8, "合计成本", String.format("%.2f", header.getTotalCost()), style);
    }

    private void createHeaderCell(Row row, int col, String label, String value, CellStyle headerStyle) {
        Cell labelCell = row.createCell(col);
        labelCell.setCellValue(label);
        labelCell.setCellStyle(headerStyle);

        Cell valueCell = row.createCell(col + 1);
        valueCell.setCellValue(value);
        valueCell.setCellStyle(this.valueStyle);
    }

    private void createTableHeader(Sheet sheet, CellStyle style) {
        Row headerRow = sheet.createRow(6);
        String[] headers = {"料号", "品名", "型号", "计量单位", "用量", "厂家/品牌", "途径/渠道", "单价", "合计", "备注"};
        for (int i = 0; i < headers.length; i++) {
            Cell cell = headerRow.createCell(i);
            cell.setCellValue(headers[i]);
            cell.setCellStyle(style);
        }
    }

    private void fillData(Sheet sheet, List<BOMItem> items, CellStyle style) {
        int rowNum = 7;
        for (BOMItem item : items) {
            Row row = sheet.createRow(rowNum++);
            row.createCell(0).setCellValue(item.getMaterialCode());
            row.createCell(1).setCellValue(item.getName());
            row.createCell(2).setCellValue(item.getModelNumber());
            row.createCell(3).setCellValue(item.getUnit());
            row.createCell(4).setCellValue(item.getQuantity());
            row.createCell(5).setCellValue(item.getManufacturer());
            row.createCell(6).setCellValue(item.getChannel());
            row.createCell(7).setCellValue(item.getUnitPrice());
            row.createCell(8).setCellValue(item.getTotal());
            row.createCell(9).setCellValue(item.getRemarks());
            
            for (int i = 0; i < 10; i++) {
                row.getCell(i).setCellStyle(style);
            }
        }
    }

    private void createNote(Sheet sheet, CellStyle style) {
        Row noteRow = sheet.createRow(sheet.getLastRowNum() + 1);
        
        // 创建所有需要合并的单元格并设置样式
        for (int i = 0; i < 10; i++) {
            Cell cell = noteRow.createCell(i);
            cell.setCellStyle(style);
            // 只在第一个单元格设置值
            if (i == 0) {
                cell.setCellValue("说明:采购渠道请与采购部门进行确认,如物料采购困难,请及时与研发人员沟通更换其他替代品");
            }
        }
        
        // 合并单元格
        sheet.addMergedRegion(new CellRangeAddress(noteRow.getRowNum(), noteRow.getRowNum(), 0, 9));
    }

    private void setColumnWidths(Sheet sheet) {
        int[] widths = {15, 15, 15, 12, 12, 12, 15, 12, 15, 12};
        for (int i = 0; i < widths.length; i++) {
            sheet.setColumnWidth(i, widths[i] * 256);
        }
    }

    public byte[] generateBOMExcel(BOMHeader header, List<BOMItem> items) throws IOException {
        try (Workbook workbook = new XSSFWorkbook()) {
            Sheet sheet = workbook.createSheet("BOM物料清单");
            
            // 创建样式
            CellStyle headerStyle = createHeaderStyle(workbook);
            CellStyle normalStyle = createNormalStyle(workbook);
            this.valueStyle = createValueStyle(workbook);  // 初始化值样式
            
            // 设置标题
            createTitle(sheet, headerStyle);

            // 设置基本信息
            createBasicInfo(sheet, header, headerStyle);

            // 创建表头
            createTableHeader(sheet, headerStyle);

            // 填充数据
            fillData(sheet, items, normalStyle);

            // 添加说明
            createNote(sheet, normalStyle);

            // 调整列宽
            setColumnWidths(sheet);

            // 导出
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            workbook.write(outputStream);
            return outputStream.toByteArray();
        }
    }

    // 该方法用于创建 Excel 表头的单元格样式
    private CellStyle createHeaderStyle(Workbook workbook) {
        // 创建一个新的单元格样式
        CellStyle style = workbook.createCellStyle();
        // 设置单元格的前景填充颜色为浅绿
        style.setFillForegroundColor(IndexedColors.LIGHT_GREEN.getIndex());
        // 设置填充模式为纯色填充
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        // 设置上边框为细线
        style.setBorderTop(BorderStyle.THIN);
        // 设置下边框为细线
        style.setBorderBottom(BorderStyle.THIN);
        // 设置左边框为细线
        style.setBorderLeft(BorderStyle.THIN);
        // 设置右边框为细线
        style.setBorderRight(BorderStyle.THIN);
        // 设置水平对齐方式为居中
        style.setAlignment(HorizontalAlignment.CENTER);
        // 设置垂直对齐方式为居中
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        // 返回创建好的单元格样式
        return style;
    }

    // 该方法用于创建 Excel 普通单元格的样式
    private CellStyle createNormalStyle(Workbook workbook) {
        // 创建一个新的单元格样式
        CellStyle style = workbook.createCellStyle();
        // 设置上边框为细线
        style.setBorderTop(BorderStyle.THIN);
        // 设置下边框为细线
        style.setBorderBottom(BorderStyle.THIN);
        // 设置左边框为细线
        style.setBorderLeft(BorderStyle.THIN);
        // 设置右边框为细线
        style.setBorderRight(BorderStyle.THIN);
        // 设置水平对齐方式为居中
        style.setAlignment(HorizontalAlignment.CENTER);
        // 设置垂直对齐方式为居中
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        // 返回创建好的单元格样式
        return style;
    }

    // 此方法用于创建 Excel 中存储值的单元格的样式
    private CellStyle createValueStyle(Workbook workbook) {
        // 创建一个新的单元格样式对象
        CellStyle style = workbook.createCellStyle();
        // 设置上边框为细线
        style.setBorderTop(BorderStyle.THIN);
        // 设置下边框为细线
        style.setBorderBottom(BorderStyle.THIN);
        // 设置左边框为细线
        style.setBorderLeft(BorderStyle.THIN);
        // 设置右边框为细线
        style.setBorderRight(BorderStyle.THIN);
        // 设置单元格内容的水平对齐方式为居中
        style.setAlignment(HorizontalAlignment.CENTER);
        // 设置单元格内容的垂直对齐方式为居中
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        // 将创建好的单元格样式对象返回
        return style;
    }
} 

获取源码

源码地址:源码地址

写到这里也就结束了,如果你觉得此文章对你有所帮助的话就一键三连,在下谢谢您嘞!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2279140.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Web】2025西湖论剑·中国杭州网络安全安全技能大赛题解(全)

目录 Rank-l Rank-U sqli or not Rank-l username存在报错回显&#xff0c;发现可以打SSTI 本地起一个服务&#xff0c;折半查找fuzz黑名单&#xff0c;不断扔给fenjing去迭代改payload from flask import Flask, request, render_template_stringapp Flask(__name__)app…

索引(MySQL)

1. 没有索引&#xff0c;可能会有什么问题 索引&#xff1a;提高数据库的性能&#xff0c;索引是物美价廉的东西了。不用加内存&#xff0c;不用改程序&#xff0c;不用调sql&#xff0c;只要执行 正确的 create index &#xff0c;查询速度就可能提高成百上千倍。但是天下没有…

C#在Json序列化时将key和value转为对应的中文

在C#中&#xff0c;实体类可以通过System.Text.Json或Newtonsoft.Json库等方式直接序列化为json字符串&#xff0c;key为字段&#xff08;属性&#xff09;名&#xff0c;value为值。 上面的方式虽然实现简单&#xff0c;但是有个缺陷&#xff0c;就是转化后的json给外人展示时…

23- TIME-LLM: TIME SERIES FORECASTING BY REPRO- GRAMMING LARGE LANGUAGE MODELS

解决问题 用LLM来解决时序预测问题&#xff0c;并且能够将时序数据映射&#xff08;reprogramming&#xff09;为NLP token&#xff0c;并且保持backbone的大模型是不变的。解决了时序序列数据用于大模型训练数据稀疏性的问题。 方法 Input Embedding 输入&#xff1a; X …

使用 Java 开发 Android 应用:Kotlin 与 Java 的混合编程

使用 Java 开发 Android 应用&#xff1a;Kotlin 与 Java 的混合编程 在开发 Android 应用程序时&#xff0c;我们通常可以选择使用 Java 或 Kotlin 作为主要的编程语言。然而&#xff0c;有些开发者可能会想要在同一个项目中同时使用这两种语言&#xff0c;这就是所谓的混合编…

左神算法基础提升--4

文章目录 树形dp问题Morris遍历 树形dp问题 求解这个问题需要用到我们在基础班上学到的从节点的左子树和右子树上拿信息的方法。 求最大距离主要分为两种情况&#xff1a;1.当前节点参与最大距离的求解&#xff1b;2.当前节点不参与最大距离的求解&#xff1b; 1.当前节点参与最…

拆分工作簿转换PDF格式文件一步到位-Excel易用宝

今天一大早老板急匆匆的跑进办公室说&#xff0c;快快快&#xff0c;&#xff0c;快把这个工作簿中的工作表拆分出来&#xff0c;然后转换成PDF格式文件发给客户。 我一看这么多工作表&#xff0c;又是拆分表格&#xff0c;又是转换文件&#xff0c;这么复杂的工作&#xff0c…

SSM课设-学生管理系统

【课设者】SSM课设-学生管理系统 技术栈: 后端: SpringSpringMVCMybatisMySQLJSP 前端: HtmlCssJavaScriptEasyUIAjax 功能: 学生端: 登陆 学生信息管理 个人信息管理 老师端: 多了教师信息管理 管理员端: 多了班级信息管理 多了年级信息管理 多了系统用户管理

Python调用go语言编译的库

要在 Python 中调用用 Go 语言编写的库&#xff0c;可以使用 Go 语言的 cgo 特性将 Go 代码编译成共享库&#xff08;如 .so 文件&#xff09;&#xff0c;然后在 Python 中通过 ctypes 或 cffi 模块加载和调用这个共享库。 新建main.go文件&#xff0c;使用go语言编写如下代码…

JavaWeb简单开发

JavaWeb 开发是指基于 Java 技术栈进行 Web 应用开发的过程&#xff0c;主要依赖于 Java EE 或者 Spring 框架来构建服务器端应用。JavaWeb 的技术栈比较广泛&#xff0c;通常包括以下几个部分&#xff1a; 示例&#xff1a;简单的 JavaWeb 应用&#xff08;Spring Boot Thyme…

[Mac + Icarus Verilog + gtkwave] Mac运行Verilog及查看波形图

目录 1. MAC安装环境 1. 1 Icarus Verilog 编译 1. 2 gtkwave 查看波形 2. 安装遇到的问题 2. 1 macOS cannot verify that this app is free from malware 2. 2 gtkwave-bin is not compatible with macOS 14 or later 3. 运行示例 3. 1 源代码 3. 2 编译Verilog 3. 3 生成.v…

Phi小模型开发教程:用C#开发本地部署AI聊天工具,只需CPU,不需要GPU,3G内存就可以运行,不输GPT-3.5

大家好&#xff0c;我是编程乐趣。 行业诸多大佬一直在说&#xff1a;“‌2025年将是AI应用元年‌”&#xff0c;虽然说大佬的说法不一定对&#xff0c;但AI趋势肯定没错的。 对于我们程序员来说&#xff0c;储备AI应用开发技能&#xff0c;不管对找工作、接项目、创业肯定是…

物联网网关Web服务器--Boa服务器移植与测试

1、Boa服务器介绍 BOA 服务器是一个小巧高效的web服务器&#xff0c;是一个运行于unix或linux下的&#xff0c;支持CGI的、适合于嵌入式系统的单任务的http服务器&#xff0c;源代码开放、性能高。 Boa 嵌入式 web 服务器的官方网站是http://www.boa.org/。 特点 轻量级&#x…

Maven的多模块架构设计诺依的多模块

Maven的多模块架构设计 多模块架构设计&#xff0c;本文采用 诺依的多模块架构设计分析

.Net 6.0 .Net7.0 .Net8.0 .Net9.0 使用 Serilog 按日志等级写入日志及 appsetting.json 配置方式实现

前言 最近使用最新版的Serilog记录日志时&#xff0c;发现以前有些关于Serilog的Nuget弃用了&#xff0c;最关键的是有些配置写法也改变&#xff0c;于是就整理了一下最新版的Serilog配置方式(appsetting.json)的使用 说明&#xff1a;我是用的.Net6&#xff0c;最新长期支持…

数字化时代,传统代理模式的变革之路

在数字化飞速发展的今天&#xff0c;线上线下融合&#xff08;O2O&#xff09;成了商业领域的大趋势。这股潮流&#xff0c;正猛烈冲击着传统代理模式&#xff0c;给它带来了新的改变。 咱们先看看线上线下融合现在啥情况。线上渠道那是越来越多&#xff0c;企业纷纷在电商平台…

wireshark上没有显示出来rtp协议如何处理

分析》启用的协议 搜索rtp&#xff0c;勾选上rtp_udp即可。

edge浏览器恢复旧版滚动条

1、地址栏输入edge://flags 2、搜索Fluent scrollbars.&#xff0c;选择disabled&#xff0c;重启即可

使用 Java 和 FreeMarker 实现自动生成供货清单,动态生成 Word 文档,简化文档处理流程。

在上一篇博客中主要是使用SpringBootApache POI实现了BOM物料清单Excel表格导出&#xff0c;详见以下博客&#xff1a; Spring Boot Apache POI 实现 Exc&#xff08;&#xff09;el 导出&#xff1a;BOM物料清单生成器&#xff08;支持中文文件名、样式美化、数据合并&#…

二进制/源码编译安装mysql 8.0

二进制方式&#xff1a; 1.下载或上传安装包至设备&#xff1a; 2.创建组与用户&#xff1a; [rootopenEuler-1 ~]# groupadd mysql [rootopenEuler-1 ~]# useradd -r -g mysql -s /bin/false mysql 3.解压安装包&#xff1a; tar xf mysql-8.0.36-linux-glibc2.12-x86_64.ta…