From 16d692734f2f764ae177f4c8b2f0b940de947ffc Mon Sep 17 00:00:00 2001 From: "Emil.Zhang" Date: Thu, 27 Apr 2023 11:25:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(Excel=E5=AF=BC=E5=87=BA=E9=99=84=E5=B8=A6?= =?UTF-8?q?=E6=9C=89=E4=B8=8B=E6=8B=89=E6=A1=86):?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加带有下拉框数据的Excel表格 注意:下拉框内选项不能以数字开头,选项内容只允许使用中英文字符数字,如果需要解析选项内容,使用_作为区分手段 例如 安徽省_1 合肥市_101 等 --- .../com/ruoyi/common/annotation/DropDown.java | 21 + .../ruoyi/common/excel/ExcelDownHandler.java | 381 ++++++++++++++++++ .../com/ruoyi/common/utils/poi/ExcelUtil.java | 49 ++- 3 files changed, 447 insertions(+), 4 deletions(-) create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/annotation/DropDown.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/excel/ExcelDownHandler.java diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DropDown.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DropDown.java new file mode 100644 index 000000000..f474b3cd1 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/DropDown.java @@ -0,0 +1,21 @@ +package com.ruoyi.common.annotation; + +import java.lang.annotation.*; + +/** + * Excel下拉注解 + *

注解在实体类中的属性上,与@ExcelProperty同级

+ * + * @author slYe + * @author Emil.Zhang + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface DropDown { + /** + * 下拉框可选值,用于简单的指定可选值 + *

例如: @DropDown({"选项1","选项2"})

+ */ + String[] value(); +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/excel/ExcelDownHandler.java b/ruoyi-common/src/main/java/com/ruoyi/common/excel/ExcelDownHandler.java new file mode 100644 index 000000000..2c3a9daa1 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/excel/ExcelDownHandler.java @@ -0,0 +1,381 @@ +package com.ruoyi.common.excel; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.excel.write.handler.SheetWriteHandler; +import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; +import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; +import com.ruoyi.common.annotation.DropDown; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.spring.SpringUtils; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddressList; +import org.apache.poi.ss.util.WorkbookUtil; +import org.apache.poi.xssf.usermodel.XSSFDataValidation; + +import java.lang.reflect.Field; +import java.util.*; + +/** + * Excel表格下拉选 + * + * @author Emil.Zhang + */ +@Slf4j +public class ExcelDownHandler implements SheetWriteHandler { + + /** + * Excel表格中的列名 + */ + private static final String EXCEL_COLUMN_NAME = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + /** + * 单选数据Sheet名 + */ + private static final String OPTIONS_SHEET_NAME = "options"; + /** + * 联动选择数据Sheet名 + */ + private static final String LINKED_OPTIONS_SHEET_NAME = "linkedOptions"; + /** + * 是否隐藏数据Sheet + */ + private static final Boolean HIDDEN_FLAG = !"dev".equals(SpringUtils.getActiveProfile()); + /** + * 下拉可选项 + */ + private final List dropDownOptions; + /** + * 当前单选进度 + */ + private int currentOptionsColumnIndex; + /** + * 当前联动选择进度 + */ + private int currentLinkedOptionsSheetIndex; + + public ExcelDownHandler(List options) { + this.dropDownOptions = options; + this.currentOptionsColumnIndex = 0; + this.currentLinkedOptionsSheetIndex = 0; + } + + /** + * 开始创建下拉数据 + */ + @Override + public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { + Sheet sheet = writeSheetHolder.getSheet(); + // 开始设置下拉框 HSSFWorkbook + DataValidationHelper helper = sheet.getDataValidationHelper(); + Field[] fields = writeWorkbookHolder.getClazz().getDeclaredFields(); + Workbook workbook = writeWorkbookHolder.getWorkbook(); + int length = fields.length; + for (int i = 0; i < length; i++) { + if (fields[i].isAnnotationPresent(DropDown.class)) { + List options = Arrays.asList(fields[i].getDeclaredAnnotation(DropDown.class).value()); + if (options.size() > 20) { + dropDownWithSheet(helper, workbook, sheet, i, options); + } else { + dropDownWithSimple(helper, sheet, i, options); + } + } + } + dropDownOptions.forEach(everyOptions -> { + // 如果传递了下拉框选择器参数 + if (!everyOptions.getNextOptions().isEmpty()) { + // 当二级选项不为空时,使用额外关联表的形式 + dropDownLinkedOptions(helper, workbook, sheet, everyOptions); + } else if (everyOptions.getOptions().size() > 10) { + // 当一级选项参数个数大于10,使用额外表的形式 + dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions()); + } else if (everyOptions.getOptions().size() != 0) { + // 当一级选项个数不为空,使用默认形式 + dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions()); + } + // 否则不做处理 + }); + } + + /** + * 简单下拉框 + */ + private void dropDownWithSimple(DataValidationHelper helper, Sheet sheet, Integer celIndex, List value) { + if (value.isEmpty()) { + return; + } + this.markOptionsToSheet(helper, sheet, celIndex, helper.createExplicitListConstraint(value.toArray(new String[0]))); + } + + /** + * 额外表格形式的级联下拉框 + */ + private void dropDownLinkedOptions(DataValidationHelper helper, Workbook workbook, Sheet sheet, DropDownOptions options) { + String linkedOptionsSheetName = String.format("%s_%d", LINKED_OPTIONS_SHEET_NAME, currentLinkedOptionsSheetIndex); + // 创建联动下拉数据表 + Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName)); + // 将下拉表隐藏 + workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), HIDDEN_FLAG); + // 完善横向的一级选项数据表 + List firstOptions = options.getOptions(); + Map> secoundOptionsMap = options.getNextOptions(); + + // 创建名称管理器 + Name name = workbook.createName(); + // 设置名称管理器的别名 + name.setNameName(linkedOptionsSheetName); + // 以横向第一行创建一级下拉拼接引用位置 + String firstOptionsFunction = String.format("%s!$%s$1:$%s$1", + linkedOptionsSheetName, + getExcelColumnName(0), + getExcelColumnName(firstOptions.size()) + ); + // 设置名称管理器的引用位置 + name.setRefersToFormula(firstOptionsFunction); + // 设置数据校验为序列模式,引用的是名称管理器中的别名 + this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName)); + + for (int columIndex = 0; columIndex < firstOptions.size(); columIndex++) { + // 先提取主表中一级下拉的列名 + String firstOptionsColumnName = getExcelColumnName(columIndex); + // 一次循环是每一个一级选项 + int finalI = columIndex; + // 本次循环的一级选项值 + String thisFirstOptionsValue = firstOptions.get(columIndex); + // 创建第一行的数据 + Optional + // 获取第一行 + .ofNullable(linkedOptionsDataSheet.getRow(0)) + // 如果不存在则创建第一行 + .orElseGet(() -> linkedOptionsDataSheet.createRow(finalI)) + // 第一行当前列 + .createCell(columIndex) + // 设置值为当前一级选项值 + .setCellValue(thisFirstOptionsValue); + + // 第二行开始,设置第二级别选项参数 + List secondOptions = secoundOptionsMap.get(thisFirstOptionsValue); + if (ObjectUtil.isEmpty(secondOptions)) { + // 必须保证至少有一个关联选项,否则将导致Excel解析错误 + secondOptions = Collections.singletonList("暂无_0"); + } + + // 以该一级选项值创建子名称管理器 + Name sonName = workbook.createName(); + // 设置名称管理器的别名 + sonName.setNameName(thisFirstOptionsValue); + // 以第二行该列数据拼接引用位置 + String sonFunction = String.format("%s!$%s$2:$%s$%d", + linkedOptionsSheetName, + firstOptionsColumnName, + firstOptionsColumnName, + secondOptions.size() + 1 + ); + // 设置名称管理器的引用位置 + sonName.setRefersToFormula(sonFunction); + // 数据验证为序列模式,引用到每一个主表中的二级选项位置 + // 创建子项的名称管理器,只是为了使得Excel可以识别到数据 + String mainSheetFirstOptionsColumnName = getExcelColumnName(options.getIndex()); + for (int i = 0; i < 100; i++) { + // 以一级选项对应的主体所在位置创建二级下拉 + String secondOptionsFunction = String.format("=INDIRECT(%s%d)", mainSheetFirstOptionsColumnName, i + 1); + // 二级只能主表每一行的每一列添加二级校验 + markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction)); + } + + for (int rowIndex = 0; rowIndex < secondOptions.size(); rowIndex++) { + // 从第二行开始填充二级选项 + int finalRowIndex = rowIndex + 1; + int finalColumIndex = columIndex; + + Row row = Optional + .ofNullable(linkedOptionsDataSheet.getRow(finalRowIndex)) + // 没有则创建 + .orElseGet(() -> linkedOptionsDataSheet.createRow(finalRowIndex)); + Optional + // 在本级一级选项所在的列 + .ofNullable(row.getCell(finalColumIndex)) + // 不存在则创建 + .orElseGet(() -> row.createCell(finalColumIndex)) + // 设置二级选项值 + .setCellValue(secondOptions.get(rowIndex)); + } + } + + currentLinkedOptionsSheetIndex++; + } + + /** + * 额外表格形式的下拉框 + */ + private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List value) { + // 创建下拉数据表 + Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME))) + .orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME))); + // 将下拉表隐藏 + workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), HIDDEN_FLAG); + // 完善纵向的一级选项数据表 + for (int i = 0; i < value.size(); i++) { + int finalI = i; + // 获取每一选项行,如果没有则创建 + Row row = Optional.ofNullable(simpleDataSheet.getRow(i)) + .orElseGet(() -> simpleDataSheet.createRow(finalI)); + // 获取本级选项对应的选项列,如果没有则创建 + Cell cell = Optional.ofNullable(row.getCell(currentOptionsColumnIndex)) + .orElseGet(() -> row.createCell(currentOptionsColumnIndex)); + // 设置值 + cell.setCellValue(value.get(i)); + } + + // 创建名称管理器 + Name name = workbook.createName(); + // 设置名称管理器的别名 + String nameName = String.format("%s_%d", OPTIONS_SHEET_NAME, celIndex); + name.setNameName(nameName); + // 以纵向第一列创建一级下拉拼接引用位置 + String function = String.format("%s!$%s$1:$%s$%d", + OPTIONS_SHEET_NAME, + getExcelColumnName(currentOptionsColumnIndex), + getExcelColumnName(currentOptionsColumnIndex), + value.size()); + // 设置名称管理器的引用位置 + name.setRefersToFormula(function); + // 设置数据校验为序列模式,引用的是名称管理器中的别名 + this.markOptionsToSheet(helper, sheet, celIndex, helper.createFormulaListConstraint(nameName)); + currentOptionsColumnIndex++; + } + + /** + * 挂载下拉的列,仅限一级选项 + */ + private void markOptionsToSheet(DataValidationHelper helper, + Sheet sheet, + Integer celIndex, + DataValidationConstraint constraint) { + // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 + CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, celIndex, celIndex); + markDataValidationToSheet(helper, sheet, constraint, addressList); + } + + /** + * 挂载下拉的列,仅限二级选项 + */ + private void markLinkedOptionsToSheet(DataValidationHelper helper, + Sheet sheet, + Integer rowIndex, + Integer celIndex, + DataValidationConstraint constraint) { + // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 + CellRangeAddressList addressList = new CellRangeAddressList(rowIndex, rowIndex, celIndex, celIndex); + markDataValidationToSheet(helper, sheet, constraint, addressList); + } + + /** + * 应用数据校验 + */ + private void markDataValidationToSheet(DataValidationHelper helper, + Sheet sheet, + DataValidationConstraint constraint, + CellRangeAddressList addressList) { + // 数据有效性对象 + DataValidation dataValidation = helper.createValidation(constraint, addressList); + // 处理Excel兼容性问题 + if (dataValidation instanceof XSSFDataValidation) { + //数据校验 + dataValidation.setSuppressDropDownArrow(true); + //错误提示 + dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP); + dataValidation.createErrorBox("提示", "此值与单元格定义数据不一致"); + dataValidation.setShowErrorBox(true); + //选定提示 + dataValidation.createPromptBox("填写说明:", "填写内容只能为下拉中数据,其他数据将导致导入失败"); + dataValidation.setShowPromptBox(true); + sheet.addValidationData(dataValidation); + } else { + dataValidation.setSuppressDropDownArrow(false); + } + sheet.addValidationData(dataValidation); + } + + /** + * 获取栏位名 + */ + private String getExcelColumnName(int columnIndex) { + // 26一循环的次数 + int columnCircleCount = columnIndex / 26; + // 26一循环内的位置 + int thisCircleColumnIndex = columnIndex % 26; + // 26一循环的次数大于0,则视为栏名至少两位 + String columnPrefix = columnCircleCount == 0 + ? "" + : StrUtil.subWithLength(EXCEL_COLUMN_NAME, columnCircleCount, 1); + // 从26一循环内取对应的栏位名 + String columnNext = StrUtil.subWithLength(EXCEL_COLUMN_NAME, thisCircleColumnIndex, 1); + // 将二者拼接即为最终的栏位名 + return columnPrefix + columnNext; + } + + /** + * 下拉可选项 + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class DropDownOptions { + /** + * 一级下拉所在列index,从0开始算 + */ + private int index = 0; + /** + * 二级下拉所在的index,从0开始算,不能与一级相同 + */ + private int nextIndex = 0; + /** + * 一级下拉所包含的数据 + */ + private List options = new ArrayList<>(); + /** + * 二级下拉所包含的数据Map + *

以每一个一级选项值为Key,每个一级选项对应的二级数据为Value

+ */ + private Map> nextOptions = new HashMap<>(); + + /** + * 创建只有一级的下拉选 + */ + public DropDownOptions(int index, List options) { + this.index = index; + this.options = options; + } + + /** + * 创建选项可选值 + *

注意:不能以数字,特殊符号开头,选项中不可以包含任何运算符号

+ * + * @param vars 可选值内包含的参数 + * @return 合规的可选值 + */ + public static String createOptionValue(Object... vars) { + StringBuilder stringBuffer = new StringBuilder(); + String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$"; + for (int i = 0; i < vars.length; i++) { + Object var = vars[i]; + if (!var.toString().matches(regex)) { + throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字"); + } + stringBuffer.append(StrUtil.trimToEmpty(var.toString())); + if (i < vars.length - 1) { + // 直至最后一个前,都以_作为切割线 + stringBuffer.append("_"); + } + } + if (stringBuffer.toString().matches("^\\d_*$")) { + throw new ServiceException("禁止以数字开头"); + } + return stringBuffer.toString(); + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java index cf2ba885c..fddee0ba5 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java @@ -11,10 +11,7 @@ import com.alibaba.excel.write.metadata.fill.FillConfig; import com.alibaba.excel.write.metadata.fill.FillWrapper; import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; import com.ruoyi.common.convert.ExcelBigNumberConvert; -import com.ruoyi.common.excel.CellMergeStrategy; -import com.ruoyi.common.excel.DefaultExcelListener; -import com.ruoyi.common.excel.ExcelListener; -import com.ruoyi.common.excel.ExcelResult; +import com.ruoyi.common.excel.*; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.file.FileUtils; import lombok.AccessLevel; @@ -113,6 +110,31 @@ public class ExcelUtil { } } + /** + * 导出带有下拉框的Excel表格 + * + * @param options 下拉框数据 + */ + public static void exportExcel(List list, String sheetName, Class clazz, + HttpServletResponse response, List options) { + try { + resetResponse(sheetName, response); + ServletOutputStream os = response.getOutputStream(); + ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz) + .autoCloseStream(false) + // 自动适配 + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) + // 大数值自动转换 防止失真 + .registerConverter(new ExcelBigNumberConvert()) + // 添加下拉框操作 + .registerWriteHandler(new ExcelDownHandler(options)) + .sheet(sheetName); + builder.doWrite(list); + } catch (IOException e) { + throw new RuntimeException("导出Excel异常"); + } + } + /** * 导出excel * @@ -149,6 +171,25 @@ public class ExcelUtil { builder.doWrite(list); } + /** + * 导出带有下拉框的Excel表格 + * + * @param options 下拉框数据 + */ + public static void exportExcel(List list, String sheetName, Class clazz, + OutputStream os, List options) { + ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz) + .autoCloseStream(false) + // 自动适配 + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) + // 大数值自动转换 防止失真 + .registerConverter(new ExcelBigNumberConvert()) + // 添加下拉框操作 + .registerWriteHandler(new ExcelDownHandler(options)) + .sheet(sheetName); + builder.doWrite(list); + } + /** * 单表多数据模板导出 模板格式为 {.属性} *