当前位置: 首页 > news >正文

实现excel的树形导出

这个是我实现的原型图,可以先看一下是否是你想要的格式。其实我一直是不想做这个树形excel的导出的,我认为excel结构不能很清晰的展示出数据的层级关系,并且就算是展示出来了,也是很脆弱的层级关系。我们在进行excel导入的时候很难去清洗数据,并还原出树形结构。但是,公司一定要求实现这个功能,只能勉强的做了。

我查询了大量的资料,发现树形结构的构建貌似只能使用这种合并单元格的形式来实现。(还有一种是通过空格缩进的形式来构建树形结构,但是,我认为这种形式的树形结构更加的脆弱,所以。不实现这种方式)由于我们是使用合并单元格的形式,并且直接使用<层级n名称>来构建整合树形结构,所以那种传统的excel肯定是不行的,我们这边的层级是不固定的,所以,就需要动态的设置excel的表头,并且,还要合并相应的单元格。还要保证excel导入时能正确的清洗出数据。

我使用了谷歌的poi进行数据库的操作。相应的maven依赖如下:

  <dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>5.2.5</version><scope>compile</scope></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>5.2.5</version><scope>compile</scope></dependency>

我这边是使用了 《名称》这个字段来构建层级关系,你也可以使用其他的字段(但是,一般都要保证这个字段在数据库中是唯一的)由于我们在导出excel时通常是不导出id的,所以,唯一字段就显得格外的重要。我们再导入时根据这个唯一字段来进行区分excel中的数据是新增或者修改。我的业务代码啊中,要保证《名称》和《编码》这两个字段的组合唯一。

还要注意的是,我的树形结构构建时还要有一个属性:leveType,这个字段是用来展示数据的层级关系的(一般来说,只要是树形结构都会有这个字段的。如果,你的树形结构没有的话,那么在进行excel的导出时就要自己再构建这个属性)。我的方法需要得到一个以及构建好层级关系的树形列表:List<T> 这样,再导出时就可以构建树形结构了。

并且,再导入方法时,最成功的构建了树形关系。这时,可以进行数据库的操作了。(可以在数据清洗时,就直接判断出那些数据是新增/修改的)

相应的代码如下:

主逻辑,excel的导入、导出

package com.scmpt.templates.infrastructure.domains;import com.scmpt.framework.core.httpException.ResultException;
import com.scmpt.templates.client.domains.dto.data.DomainsVo;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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 org.springframework.web.multipart.MultipartFile;import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Stack;/*** @Author 张乔* @Date 2025/9/8 14:51*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DomainsExcelService {private final TreeConvertService treeConvertService;// 常量定义private static final int MIN_COLUMN_WIDTH = 10 * 256; // 最小列宽(10个字符)private static final int MAX_COLUMN_WIDTH = 50 * 256; // 最大列宽(50个字符)private static final int DEFAULT_COLUMN_WIDTH = 15 * 256; // 默认列宽(15个字符)private static final int FIRST_LEVEL_WIDTH = 25 * 256; // 第一级列宽private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");private static final String EXCEL_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";private static final String FONT_NAME = "微软雅黑"; // 字体名称public void exportExcel(HttpServletResponse response, List<DomainsVo> treeData) {if (treeData == null || treeData.isEmpty()) {return;}// 1. 将树形结构转换为平面列表treeConvertService.convertTreeToFlatList(treeData);int maxLevel = treeConvertService.getMaxLevel();// 2. 创建工作簿和工作表Workbook workbook = new XSSFWorkbook();Sheet sheet = workbook.createSheet("模板领域");// 3. 创建样式CellStyle headerStyle = createHeaderStyle(workbook);CellStyle dataStyle = createDataStyle(workbook);CellStyle dateStyle = createDateStyle(workbook); // 创建时间样式// 4. 创建表头createHeaderRow(sheet, headerStyle, maxLevel);// 5. 填充数据并处理合并int rowNum = 1; // 从第1行开始(第0行是表头)List<CellRangeAddress> mergeRegions = new ArrayList<>();for (DomainsVo node : treeData) {rowNum = processNode(sheet, dataStyle, dateStyle,node,rowNum, 0, new ArrayList<>(), mergeRegions, maxLevel);}// 6. 应用合并区域for (CellRangeAddress region : mergeRegions) {sheet.addMergedRegion(region);}// 7. 动态调整列宽adjustColumnWidths(sheet, maxLevel);response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("utf-8");String fileName = URLEncoder.encode("数据导出", StandardCharsets.UTF_8);response.setHeader("Content-disposition", "attachment;filename="+fileName+".xlsx");try {// 8. 写入响应流workbook.write(response.getOutputStream());workbook.close();} catch (IOException e) {log.error("导出数据出错", e);}}/*** 动态调整列宽*/private void adjustColumnWidths(Sheet sheet, int maxLevel) {// 设置层级列初始宽度for (int i = 0; i < maxLevel; i++) {if (i == 0) {sheet.setColumnWidth(i, FIRST_LEVEL_WIDTH);} else {sheet.setColumnWidth(i, DEFAULT_COLUMN_WIDTH);}}// 设置固定列初始宽度int[] fixedColumnWidths = {15, 20, 18}; // 编码、备注、创建时间的初始宽度(字符数)for (int i = 0; i < fixedColumnWidths.length; i++) {sheet.setColumnWidth(maxLevel + i, fixedColumnWidths[i] * 256);}// 基于内容自动调整列宽,但限制在最小和最大宽度之间for (int i = 0; i < maxLevel + 3; i++) {sheet.autoSizeColumn(i);int currentWidth = sheet.getColumnWidth(i);// 确保列宽在合理范围内if (currentWidth < MIN_COLUMN_WIDTH) {sheet.setColumnWidth(i, MIN_COLUMN_WIDTH);} else if (currentWidth > MAX_COLUMN_WIDTH) {sheet.setColumnWidth(i, MAX_COLUMN_WIDTH);}}}/*** 创建表头样式*/private CellStyle createHeaderStyle(Workbook workbook) {CellStyle style = workbook.createCellStyle();// 设置字体Font font = workbook.createFont();font.setBold(true);font.setFontHeightInPoints((short) 12);font.setFontName(FONT_NAME);font.setColor(IndexedColors.WHITE.getIndex());style.setFont(font);// 设置对齐方式style.setAlignment(HorizontalAlignment.CENTER);style.setVerticalAlignment(VerticalAlignment.CENTER);// 设置边框style.setBorderBottom(BorderStyle.THIN);style.setBorderTop(BorderStyle.THIN);style.setBorderLeft(BorderStyle.THIN);style.setBorderRight(BorderStyle.THIN);// 设置背景色style.setFillForegroundColor(IndexedColors.DARK_BLUE.getIndex());style.setFillPattern(FillPatternType.SOLID_FOREGROUND);return style;}/*** 创建数据样式*/private CellStyle createDataStyle(Workbook workbook) {CellStyle style = workbook.createCellStyle();// 设置字体Font font = workbook.createFont();font.setFontHeightInPoints((short) 11);font.setFontName(FONT_NAME);style.setFont(font);// 设置对齐方式style.setAlignment(HorizontalAlignment.LEFT);style.setVerticalAlignment(VerticalAlignment.CENTER);// 设置边框style.setBorderBottom(BorderStyle.THIN);style.setBorderTop(BorderStyle.THIN);style.setBorderLeft(BorderStyle.THIN);style.setBorderRight(BorderStyle.THIN);style.setWrapText(true);return style;}/*** 创建时间样式*/private CellStyle createDateStyle(Workbook workbook) {CellStyle style = workbook.createCellStyle();// 设置字体Font font = workbook.createFont();font.setFontHeightInPoints((short) 11);font.setFontName(FONT_NAME);style.setFont(font);// 设置对齐方式style.setAlignment(HorizontalAlignment.LEFT);style.setVerticalAlignment(VerticalAlignment.CENTER);// 设置边框style.setBorderBottom(BorderStyle.THIN);style.setBorderTop(BorderStyle.THIN);style.setBorderLeft(BorderStyle.THIN);style.setBorderRight(BorderStyle.THIN);// 设置时间格式CreationHelper createHelper = workbook.getCreationHelper();style.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_FORMAT));return style;}/*** 创建表头行*/private void createHeaderRow(Sheet sheet, CellStyle style, int maxLevel) {Row headerRow = sheet.createRow(0);headerRow.setHeightInPoints(25); // 设置表头行高// 创建层级列(从第0列开始)for (int i = 0; i < maxLevel; i++) {Cell cell = headerRow.createCell(i);cell.setCellValue("层级" + (i + 1) + "名称");cell.setCellStyle(style);}// 创建固定列String[] fixedHeaders = {"编码", "备注", "创建时间"};for (int i = 0; i < fixedHeaders.length; i++) {Cell cell = headerRow.createCell(maxLevel + i);cell.setCellValue(fixedHeaders[i]);cell.setCellStyle(style);}}/*** 处理节点数据*/private int processNode(Sheet sheet, CellStyle style,CellStyle dateStyle,DomainsVo node, int rowNum,Integer level, List<DomainsVo> parentNodes,List<CellRangeAddress> mergeRegions, int maxLevel) {if (node == null) return rowNum;// 创建当前行Row row = sheet.createRow(rowNum);// 设置父级节点数据(从第0列开始)for (int i = 0; i < parentNodes.size(); i++) {Cell cell = row.createCell(i);cell.setCellValue(parentNodes.get(i).getName());cell.setCellStyle(style);}// 设置当前节点数据int currentCol = parentNodes.size();Cell cell = row.createCell(currentCol);cell.setCellValue(node.getName());cell.setCellStyle(style);// 记录当前节点位置,用于后续合并// 递归处理子节点List<DomainsVo> newParentNodes = new ArrayList<>(parentNodes);newParentNodes.add(node);int childRowNum = rowNum + 1;if (node.getChildren() != null && !node.getChildren().isEmpty()) {for (DomainsVo child : node.getChildren()) {childRowNum = processNode(sheet, style,dateStyle, child, childRowNum, level + 1,newParentNodes, mergeRegions, maxLevel);}}// 如果需要合并,添加合并区域if (childRowNum - rowNum > 1) {mergeRegions.add(new CellRangeAddress(rowNum, childRowNum - 1, currentCol, currentCol));}// 设置其他固定列数据Cell codeCell = row.createCell(maxLevel);codeCell.setCellValue(node.getSoleCode() != null ? node.getSoleCode() : "");codeCell.setCellStyle(style);Cell remarkCell = row.createCell(maxLevel + 1);remarkCell.setCellValue(node.getRemark() != null ? node.getRemark() : "");remarkCell.setCellStyle(style);Cell timeCell = row.createCell(maxLevel + 2);timeCell.setCellValue(node.getCreatedTime() != null ?node.getCreatedTime() : new Date());timeCell.setCellStyle(node.getCreatedTime() != null ? dateStyle :style);return childRowNum;}/*** 导入Excel文件并转换为树形结构** @param file 上传的Excel文件*/public Object importExcel(MultipartFile file) {try (Workbook workbook = new XSSFWorkbook(file.getInputStream())) {Sheet sheet = workbook.getSheetAt(0);// 1. 读取表头,确定最大层级int maxLevel = determineMaxLevel(sheet);// 2. 解析数据行List<ExcelRowData> rowDataList = parseRowData(sheet, maxLevel);// 3. 重建树形结构List<DomainsVo> list = rebuildTreeStructure(rowDataList, maxLevel);log.info("导入数据成功,树形结构已重建");for (DomainsVo node : list){log.info("节点数据:{}", node);}return list;} catch (IOException e) {log.error("读取Excel文件失败", e);throw new ResultException(501,"读取Excel文件失败");}}/*** 根据表头确定最大层级数*/private int determineMaxLevel(Sheet sheet) {Row headerRow = sheet.getRow(0);if (headerRow == null) {return 0;}int level = 0;for (int i = 0; i < headerRow.getLastCellNum(); i++) {Cell cell = headerRow.getCell(i);if (cell != null && cell.getStringCellValue().startsWith("层级")) {level++;} else {break;}}return level;}/*** 解析Excel中的数据行*/private List<ExcelRowData> parseRowData(Sheet sheet, int maxLevel) {List<ExcelRowData> rowDataList = new ArrayList<>();// 从第1行开始(跳过表头)for (int i = 1; i <= sheet.getLastRowNum(); i++) {Row row = sheet.getRow(i);if (row == null) {continue;}ExcelRowData rowData = new ExcelRowData();// 解析层级数据List<String> levelData = new ArrayList<>();for (int j = 0; j < maxLevel; j++) {Cell cell = row.getCell(j);String value = (cell != null) ? getCellValueAsString(cell) : "";levelData.add(value);}rowData.setLevelData(levelData);// 解析固定列数据if (maxLevel < row.getLastCellNum()) {Cell codeCell = row.getCell(maxLevel);rowData.setSoleCode(getCellValueAsString(codeCell));Cell remarkCell = row.getCell(maxLevel + 1);rowData.setRemark(getCellValueAsString(remarkCell));Cell timeCell = row.getCell(maxLevel + 2);String timeStr = getCellValueAsString(timeCell);try {rowData.setCreatedTime(timeStr.isEmpty() ? null : DATE_FORMAT.parse(timeStr));} catch (ParseException e) {log.warn("日期格式解析失败: {}", timeStr);rowData.setCreatedTime(null);}}rowDataList.add(rowData);}return rowDataList;}/*** 获取单元格的字符串值*/private String getCellValueAsString(Cell cell) {if (cell == null) {return "";}switch (cell.getCellType()) {case STRING:return cell.getStringCellValue().trim();case NUMERIC:if (DateUtil.isCellDateFormatted(cell)) {return DATE_FORMAT.format(cell.getDateCellValue());} else {// 数字格式,避免科学计数法和浮点数问题double numericValue = cell.getNumericCellValue();if (numericValue == (long) numericValue) {return String.valueOf((long) numericValue);} else {return String.valueOf(numericValue);}}case BOOLEAN:return String.valueOf(cell.getBooleanCellValue());case FORMULA:return cell.getCellFormula();default:return "";}}/*** 重建树形结构*/private List<DomainsVo> rebuildTreeStructure(List<ExcelRowData> rowDataList, int maxLevel) {List<DomainsVo> rootNodes = new ArrayList<>();Stack<DomainsVo> nodeStack = new Stack<>();Stack<Integer> levelStack = new Stack<>();for (ExcelRowData rowData : rowDataList) {// 确定当前节点的层级int currentLevel = 0;for (int i = 0; i < maxLevel; i++) {if (!rowData.getLevelData().get(i).isEmpty()) {currentLevel = i;}}// 创建当前节点DomainsVo currentNode = new DomainsVo();currentNode.setName(rowData.getLevelData().get(currentLevel));currentNode.setSoleCode(rowData.getSoleCode());currentNode.setRemark(rowData.getRemark());currentNode.setCreatedTime(rowData.getCreatedTime());currentNode.setChildren(new ArrayList<>());// 处理层级关系if (nodeStack.isEmpty()) {// 根节点rootNodes.add(currentNode);nodeStack.push(currentNode);levelStack.push(currentLevel);} else {// 寻找父节点while (!levelStack.isEmpty() && currentLevel <= levelStack.peek()) {nodeStack.pop();levelStack.pop();}if (!nodeStack.isEmpty()) {// 添加到父节点的子节点列表nodeStack.peek().getChildren().add(currentNode);} else {// 应该不会发生,但为了安全rootNodes.add(currentNode);}nodeStack.push(currentNode);levelStack.push(currentLevel);}}return rootNodes;}}
package com.scmpt.templates.infrastructure.domains;import lombok.Data;import java.util.Date;
import java.util.List;
@Data
public  class ExcelRowData {/*** 层级数据*/private List<String> levelData;/*** 唯一编码*/private String soleCode;/*** 备注*/private String remark;/*** 创建时间*/private Date createdTime;
}

相应的辅助类

package com.scmpt.templates.infrastructure.domains;import lombok.Data;import java.util.LinkedHashMap;
import java.util.Map;/*** 动态数据VO*/
@Data
public class DynamicDataVO {// 使用Map存储动态层级数据,key为列索引,value为单元格值private Map<Integer, String> levelData = new LinkedHashMap<>();// 其他固定字段private String soleCode;private String remark;// 用于合并单元格的辅助字段private transient int levelType;private transient String parentPath;/*** 设置层级数据*/public void setLevelData(int columnIndex, String value) {levelData.put(columnIndex, value);}/*** 获取层级数据*/public String getLevelData(int columnIndex) {return levelData.getOrDefault(columnIndex, "");}}

树形结构的转换,并且,算出,最大层级数。

package com.scmpt.templates.infrastructure.domains;import com.scmpt.templates.client.domains.dto.data.DomainsVo;
import lombok.Getter;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;/*** 树形结构转换服务*/
@Getter
@Service
public class TreeConvertService {/*** -- GETTER --*  获取最大层级*/private int maxLevel = 0;/*** 将树形结构转换为平面列表*/public void convertTreeToFlatList(List<DomainsVo> treeData) {List<DynamicDataVO> result = new ArrayList<>();maxLevel = 0;// 首先计算最大层级calculateMaxLevel(treeData, 1);// 转换数据int index = 1;for (DomainsVo node : treeData) {index = traverseNode(node, result,index, new String[maxLevel], "", 0);}}/*** 计算最大层级*/private void calculateMaxLevel(List<DomainsVo> nodes, int currentLevel) {if (nodes == null || nodes.isEmpty()) {return;}for (DomainsVo node : nodes) {int nodeLevel = node.getLevelType() != null ? node.getLevelType() : currentLevel;if (nodeLevel > maxLevel) {maxLevel = nodeLevel;}if (node.getChildren() != null && !node.getChildren().isEmpty()) {calculateMaxLevel(node.getChildren(), nodeLevel + 1);}}}/*** 递归遍历节点*/private int traverseNode(DomainsVo node, List<DynamicDataVO> result, int index,String[] parentNames, String parentPath, int depth) {if (node == null) return index;// 创建数据VODynamicDataVO dataVO = new DynamicDataVO();int currentLevel = node.getLevelType() != null ? node.getLevelType() : (depth + 1);dataVO.setLevelType(currentLevel);// 设置当前路径String currentPath = parentPath.isEmpty() ?node.getName() : parentPath + "|" + node.getName();dataVO.setParentPath(currentPath);// 设置层级数据for (int i = 1; i < maxLevel; i++) {if (i < currentLevel - 1) {// 父级节点dataVO.setLevelData(i + 1, parentNames[i]);} else if (i == currentLevel - 1) {// 当前节点dataVO.setLevelData(i + 1, node.getName());} else {// 子级节点(空)dataVO.setLevelData(i + 1, "");}}// 设置其他字段dataVO.setSoleCode(node.getSoleCode());dataVO.setRemark(node.getRemark());// 添加到结果列表result.add(dataVO);// 递归处理子节点if (node.getChildren() != null && !node.getChildren().isEmpty()) {String[] newParentNames = new String[maxLevel];System.arraycopy(parentNames, 0, newParentNames, 0, parentNames.length);if (currentLevel - 1 >= 0 && currentLevel - 1 < maxLevel) {newParentNames[currentLevel - 1] = node.getName();}for (DomainsVo child : node.getChildren()) {index = traverseNode(child, result, index, newParentNames, currentPath, depth + 1);}}return index;}}

至此,我的excel树形结构的导入、导出就完成了。

可以,参考一下相应的代码。如果,那位同学有更好的excel树形结构的导入、导出时,一定要和我联系(直接私信我,或者在评论区下评论),非常感谢。

http://www.dtcms.com/a/392422.html

相关文章:

  • 基于Matlab的GPS/北斗系统抗脉冲与窄带干扰算法研究及仿真验证
  • linux之负载均衡Nginx+多开Tomcat
  • 浏览器私有前缀、CSS3:2D转换、动画、3D转换
  • Redis核心面试知识点汇总
  • Java面试宝典:核心基础知识精讲
  • Python9-逻辑回归-决策树
  • 神经网络核心机制深度解析:链式法则驱动下的梯度流动与参数优化
  • Spring事务和事务传播机制(半)
  • 61.[前端开发-Vue3]Day03-购物车-v-model-组件化-Vue脚手架
  • Kafka学习笔记(p1-p14)
  • C++:四大智能指针
  • Roo Code 键盘导航与快捷键
  • SQL从入门到起飞:完整学习数据库与100+练习题
  • MyBatis 动态 SQL 详解:优雅处理复杂查询场景
  • 如何看待Qt中的QObject这个类
  • utf8mb4_bin 与 utf8mb4_generate_cli区别
  • CAN总线学习(一)CAN总线通讯&硬件电路
  • 13. LangChain4j + 加入检索增加生成 RAG(知识库)
  • TriggerRecovery
  • OpenAI 开源 GPT-oss 模型:从闭源到开源的模型架构创新之路
  • 微服务技术栈一文串讲
  • 从浅入深:自编码器(AE)与变分自编码器(VAE)的核心原理与Pytorch代码讲解
  • 低功耗超宽带收发器:DW1000设备驱动API指南
  • 2012/07 JLPT听力原文 问题四
  • Redis最佳实践——性能优化技巧之集群与分片
  • springboot的注解
  • iOS App 混淆与热更新兼容实战 混淆后如何安全可靠地推送热修复(Hotfix)与灰度回滚
  • 从 0 到 1 保姆级实现C语言双向链表
  • 2 IP地址规划与设计案例分析
  • Vue 中 8 种组件通信方式