package com.ruoyi.business.utils; import com.ruoyi.business.vo.OfficialInventoryExportVo; import com.ruoyi.common.annotation.Excel; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.poi.ExcelUtil; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.xssf.streaming.SXSSFWorkbook; import java.lang.reflect.Field; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @Slf4j public class DynamicExcelUtil extends ExcelUtil { private final List dynamicHeaders; private final List fields; public DynamicExcelUtil(Class clazz, List dynamicHeaders) { super(clazz); this.dynamicHeaders = dynamicHeaders; this.fields = getAllFields(clazz); } private List getAllFields(Class clazz) { List fields = new ArrayList<>(); Class currentClass = clazz; while (currentClass != null) { fields.addAll(Arrays.asList(currentClass.getDeclaredFields())); currentClass = currentClass.getSuperclass(); } return fields; } @Override public void exportExcel(HttpServletResponse response, List list, String sheetName) { try { String validSheetName = StringUtils.isBlank(sheetName) ? "Sheet1" : sheetName; // Create workbook directly instead of using parent's createWorkbook Workbook workbook = new SXSSFWorkbook(); Sheet sheet = workbook.createSheet(validSheetName); // Create styles createCustomStyles(workbook); // Create dynamic header createDynamicHeader(sheet, workbook); // Fill data fillDynamicData(sheet, list); // Set response response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(validSheetName + ".xlsx", "UTF-8")); workbook.write(response.getOutputStream()); } catch (Exception e) { log.error("导出Excel失败", e); throw new RuntimeException("导出Excel失败: " + e.getMessage(), e); } } private void createCustomStyles(Workbook workbook) { // Create basic styles similar to parent class if needed CellStyle headerStyle = workbook.createCellStyle(); Font headerFont = workbook.createFont(); headerFont.setBold(true); headerStyle.setFont(headerFont); headerStyle.setAlignment(HorizontalAlignment.CENTER); headerStyle.setVerticalAlignment(VerticalAlignment.CENTER); } /** * 创建动态表头(父表头"煤质" + 子表头) */ private void createDynamicHeader(Sheet sheet, Workbook workbook) { // 主表头(第1行)和子表头(第2行) Row mainHeaderRow = sheet.createRow(0); Row subHeaderRow = sheet.createRow(1); // 创建表头样式(居中 + 加粗) CellStyle headerStyle = workbook.createCellStyle(); Font headerFont = workbook.createFont(); headerFont.setBold(true); headerStyle.setFont(headerFont); headerStyle.setAlignment(HorizontalAlignment.CENTER); headerStyle.setVerticalAlignment(VerticalAlignment.CENTER); int colIndex = 0; // 遍历所有字段(固定字段 + 动态字段) for (Field field : fields) { Excel excel = field.getAnnotation(Excel.class); if (excel != null) { // 1. 如果是 "煤质" 字段,跳过(后面单独处理) if (excel.name().equals("煤质")) { continue; } // 2. 处理普通字段(非 "煤质" 的列) // 2.1 在主表头(第1行)设置字段名 Cell mainHeaderCell = mainHeaderRow.createCell(colIndex); mainHeaderCell.setCellValue(excel.name()); mainHeaderCell.setCellStyle(headerStyle); // 2.2 在子表头(第2行)留空,但合并单元格使其占据两行高度 Cell subHeaderCell = subHeaderRow.createCell(colIndex); subHeaderCell.setCellStyle(headerStyle); // 保持样式一致 // 2.3 合并当前列的第1行和第2行(垂直合并) sheet.addMergedRegion(new CellRangeAddress( 0, 1, // 从第1行到第2行 colIndex, colIndex // 当前列 )); colIndex++; // 移动到下一列 } } // 3. 处理动态字段("煤质" 下的子表头) if (!dynamicHeaders.isEmpty()) { // 3.1 在主表头(第1行)设置 "煤质" 大标题(横向合并) Cell coalHeaderCell = mainHeaderRow.createCell(colIndex); coalHeaderCell.setCellValue("煤质"); coalHeaderCell.setCellStyle(headerStyle); // 横向合并(第1行,跨越多列) sheet.addMergedRegion(new CellRangeAddress( 0, 0, // 仅第1行 colIndex, colIndex + dynamicHeaders.size() - 1 // 跨越多列 )); // 3.2 在子表头(第2行)设置动态字段(如 "灰分"、"硫分") for (int i = 0; i < dynamicHeaders.size(); i++) { Cell subCell = subHeaderRow.createCell(colIndex + i); subCell.setCellValue(dynamicHeaders.get(i)); subCell.setCellStyle(headerStyle); } } } /** * 填充数据(固定字段 + 动态字段) */ private void fillDynamicData(Sheet sheet, List list) { int rowIndex = 2; // Data starts from row 3 CellStyle dataStyle = sheet.getWorkbook().createCellStyle(); dataStyle.setAlignment(HorizontalAlignment.CENTER); for (T item : list) { Row row = sheet.createRow(rowIndex++); int colIndex = 0; // Fill fixed fields for (Field field : fields) { Excel excel = field.getAnnotation(Excel.class); if (excel != null && !excel.name().equals("煤质")) { try { field.setAccessible(true); Object value = field.get(item); Cell cell = row.createCell(colIndex++); cell.setCellStyle(dataStyle); setCellValue(cell, value); } catch (Exception e) { log.error("填充固定字段失败", e); row.createCell(colIndex++).setCellValue("-"); } } } // Fill dynamic fields if (item instanceof OfficialInventoryExportVo vo) { for (String header : dynamicHeaders) { Cell cell = row.createCell(colIndex++); cell.setCellStyle(dataStyle); cell.setCellValue(vo.getDynamicProperty(header)); } } } } /** * 设置单元格值 */ private void setCellValue(Cell cell, Object value) { if (value == null) { cell.setCellValue("-"); } else if (value instanceof Number) { cell.setCellValue(((Number) value).doubleValue()); } else { cell.setCellValue(value.toString()); } } }