소스 검색

add 增加 excel 导出下拉框功能

疯狂的狮子Li 1 년 전
부모
커밋
4a9aafe853

+ 10 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DictService.java

@@ -1,5 +1,7 @@
 package org.dromara.common.core.service;
 
+import java.util.Map;
+
 /**
  * 通用 字典服务
  *
@@ -54,4 +56,12 @@ public interface DictService {
      */
     String getDictValue(String dictType, String dictLabel, String separator);
 
+    /**
+     * 获取字典下所有的字典值与标签
+     *
+     * @param dictType 字典类型
+     * @return dictValue为key,dictLabel为值组成的Map
+     */
+    Map<String, String> getAllDictByDictType(String dictType);
+
 }

+ 149 - 0
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DropDownOptions.java

@@ -0,0 +1,149 @@
+package org.dromara.common.excel.core;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.exception.ServiceException;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * <h1>Excel下拉可选项</h1>
+ * 注意:为确保下拉框解析正确,传值务必使用createOptionValue()做为值的拼接
+ *
+ * @author Emil.Zhang
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@SuppressWarnings("unused")
+public class DropDownOptions {
+    /**
+     * 一级下拉所在列index,从0开始算
+     */
+    private int index = 0;
+    /**
+     * 二级下拉所在的index,从0开始算,不能与一级相同
+     */
+    private int nextIndex = 0;
+    /**
+     * 一级下拉所包含的数据
+     */
+    private List<String> options = new ArrayList<>();
+    /**
+     * 二级下拉所包含的数据Map
+     * <p>以每一个一级选项值为Key,每个一级选项对应的二级数据为Value</p>
+     */
+    private Map<String, List<String>> nextOptions = new HashMap<>();
+    /**
+     * 分隔符
+     */
+    private static final String DELIMITER = "_";
+
+    /**
+     * 创建只有一级的下拉选
+     */
+    public DropDownOptions(int index, List<String> options) {
+        this.index = index;
+        this.options = options;
+    }
+
+    /**
+     * <h2>创建每个选项可选值</h2>
+     * <p>注意:不能以数字,特殊符号开头,选项中不可以包含任何运算符号</p>
+     *
+     * @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++) {
+            String var = StrUtil.trimToEmpty(String.valueOf(vars[i]));
+            if (!var.matches(regex)) {
+                throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字");
+            }
+            stringBuffer.append(var);
+            if (i < vars.length - 1) {
+                // 直至最后一个前,都以_作为切割线
+                stringBuffer.append(DELIMITER);
+            }
+        }
+        if (stringBuffer.toString().matches("^\\d_*$")) {
+            throw new ServiceException("禁止以数字开头");
+        }
+        return stringBuffer.toString();
+    }
+
+    /**
+     * 将处理后合理的可选值解析为原始的参数
+     *
+     * @param option 经过处理后的合理的可选项
+     * @return 原始的参数
+     */
+    public static List<String> analyzeOptionValue(String option) {
+        return StrUtil.split(option, DELIMITER, true, true);
+    }
+
+    /**
+     * 创建级联下拉选项
+     *
+     * @param parentList                  父实体可选项原始数据
+     * @param parentIndex                 父下拉选位置
+     * @param sonList                     子实体可选项原始数据
+     * @param sonIndex                    子下拉选位置
+     * @param parentHowToGetIdFunction    父类如何获取唯一标识
+     * @param sonHowToGetParentIdFunction 子类如何获取父类的唯一标识
+     * @param howToBuildEveryOption       如何生成下拉选内容
+     * @return 级联下拉选项
+     */
+    public static <T> DropDownOptions buildLinkedOptions(List<T> parentList,
+                                                         int parentIndex,
+                                                         List<T> sonList,
+                                                         int sonIndex,
+                                                         Function<T, Number> parentHowToGetIdFunction,
+                                                         Function<T, Number> sonHowToGetParentIdFunction,
+                                                         Function<T, String> howToBuildEveryOption) {
+        DropDownOptions parentLinkSonOptions = new DropDownOptions();
+        // 先创建父类的下拉
+        parentLinkSonOptions.setIndex(parentIndex);
+        parentLinkSonOptions.setOptions(
+            parentList.stream()
+                .map(howToBuildEveryOption)
+                .collect(Collectors.toList())
+        );
+        // 提取父-子级联下拉
+        Map<String, List<String>> sonOptions = new HashMap<>();
+        // 父级依据自己的ID分组
+        Map<Number, List<T>> parentGroupByIdMap =
+            parentList.stream().collect(Collectors.groupingBy(parentHowToGetIdFunction));
+        // 遍历每个子集,提取到Map中
+        sonList.forEach(everySon -> {
+            if (parentGroupByIdMap.containsKey(sonHowToGetParentIdFunction.apply(everySon))) {
+                // 找到对应的上级
+                T parentObj = parentGroupByIdMap.get(sonHowToGetParentIdFunction.apply(everySon)).get(0);
+                // 提取名称和ID作为Key
+                String key = howToBuildEveryOption.apply(parentObj);
+                // Key对应的Value
+                List<String> thisParentSonOptionList;
+                if (sonOptions.containsKey(key)) {
+                    thisParentSonOptionList = sonOptions.get(key);
+                } else {
+                    thisParentSonOptionList = new ArrayList<>();
+                    sonOptions.put(key, thisParentSonOptionList);
+                }
+                // 往Value中添加当前子集选项
+                thisParentSonOptionList.add(howToBuildEveryOption.apply(everySon));
+            }
+        });
+        parentLinkSonOptions.setNextIndex(sonIndex);
+        parentLinkSonOptions.setNextOptions(sonOptions);
+        return parentLinkSonOptions;
+    }
+}

+ 370 - 0
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java

@@ -0,0 +1,370 @@
+package org.dromara.common.excel.core;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.EnumUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.write.handler.SheetWriteHandler;
+import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
+import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
+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 org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.service.DictService;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.excel.annotation.ExcelDictFormat;
+import org.dromara.common.excel.annotation.ExcelEnumFormat;
+
+import java.lang.reflect.Field;
+import java.util.*;
+
+/**
+ * <h1>Excel表格下拉选操作</h1>
+ * 考虑到下拉选过多可能导致Excel打开缓慢的问题,只校验前1000行
+ * <p>
+ * 即只有前1000行的数据可以用下拉框,超出的自行通过限制数据量的形式,第二次输出
+ *
+ * @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";
+    /**
+     * 下拉可选项
+     */
+    private final List<DropDownOptions> dropDownOptions;
+    /**
+     * 当前单选进度
+     */
+    private int currentOptionsColumnIndex;
+    /**
+     * 当前联动选择进度
+     */
+    private int currentLinkedOptionsSheetIndex;
+    private final DictService dictService;
+
+    public ExcelDownHandler(List<DropDownOptions> options) {
+        this.dropDownOptions = options;
+        this.currentOptionsColumnIndex = 0;
+        this.currentLinkedOptionsSheetIndex = 0;
+        this.dictService = SpringUtils.getBean(DictService.class);
+    }
+
+    /**
+     * <h2>开始创建下拉数据</h2>
+     * 1.通过解析传入的@ExcelProperty同级是否标注有@DropDown选项
+     * 如果有且设置了value值,则将其直接置为下拉可选项
+     * <p>
+     * 2.或者在调用ExcelUtil时指定了可选项,将依据传入的可选项做下拉
+     * <p>
+     * 3.二者并存,注意调用方式
+     */
+    @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++) {
+            // 循环实体中的每个属性
+            // 可选的下拉值
+            List<String> options = new ArrayList<>();
+            if (fields[i].isAnnotationPresent(ExcelDictFormat.class)) {
+                // 如果指定了@ExcelDictFormat,则使用字典的逻辑
+                ExcelDictFormat format = fields[i].getDeclaredAnnotation(ExcelDictFormat.class);
+                String dictType = format.dictType();
+                String converterExp = format.readConverterExp();
+                if (StrUtil.isNotBlank(dictType)) {
+                    // 如果传递了字典名,则依据字典建立下拉
+                    Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
+                        .orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
+                        .values();
+                    options = new ArrayList<>(values);
+                } else if (StrUtil.isNotBlank(converterExp)) {
+                    // 如果指定了确切的值,则直接解析确切的值
+                    options = StrUtil.split(converterExp, format.separator(), true, true);
+                }
+            } else if (fields[i].isAnnotationPresent(ExcelEnumFormat.class)) {
+                // 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑
+                ExcelEnumFormat format = fields[i].getDeclaredAnnotation(ExcelEnumFormat.class);
+                List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
+                options = StreamUtils.toList(values, String::valueOf);
+            }
+            if (ObjectUtil.isNotEmpty(options)) {
+                // 仅当下拉可选项不为空时执行
+                // 获取列下标,默认为当前循环次数
+                int index = i;
+                if (fields[i].isAnnotationPresent(ExcelProperty.class)) {
+                    // 如果指定了列下标,以指定的为主
+                    index = fields[i].getDeclaredAnnotation(ExcelProperty.class).index();
+                }
+                if (options.size() > 20) {
+                    // 这里限制如果可选项大于20,则使用额外表形式
+                    dropDownWithSheet(helper, workbook, sheet, index, options);
+                } else {
+                    // 否则使用固定值形式
+                    dropDownWithSimple(helper, sheet, index, 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());
+            }
+        });
+    }
+
+    /**
+     * <h2>简单下拉框</h2>
+     * 直接将可选项拼接为指定列的数据校验值
+     *
+     * @param celIndex 列index
+     * @param value    下拉选可选值
+     */
+    private void dropDownWithSimple(DataValidationHelper helper, Sheet sheet, Integer celIndex, List<String> value) {
+        if (ObjectUtil.isEmpty(value)) {
+            return;
+        }
+        this.markOptionsToSheet(helper, sheet, celIndex, helper.createExplicitListConstraint(ArrayUtil.toArray(value, String.class)));
+    }
+
+    /**
+     * <h2>额外表格形式的级联下拉框</h2>
+     *
+     * @param options 额外表格形式存储的下拉可选项
+     */
+    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), true);
+        // 完善横向的一级选项数据表
+        List<String> firstOptions = options.getOptions();
+        Map<String, List<String>> 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<String> secondOptions = secoundOptionsMap.get(thisFirstOptionsValue);
+            if (CollUtil.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++;
+    }
+
+    /**
+     * <h2>额外表格形式的普通下拉框</h2>
+     * 由于下拉框可选值数量过多,为提升Excel打开效率,使用额外表格形式做下拉
+     *
+     * @param celIndex 下拉选
+     * @param value    下拉选可选值
+     */
+    private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List<String> 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), true);
+        // 完善纵向的一级选项数据表
+        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);
+    }
+
+    /**
+     * <h2>依据列index获取列名英文</h2>
+     * 依据列index转换为Excel中的列名英文
+     * <p>例如第1列,index为0,解析出来为A列</p>
+     * 第27列,index为26,解析为AA列
+     * <p>第28列,index为27,解析为AB列</p>
+     *
+     * @param columnIndex 列index
+     * @return 列index所在得英文名
+     */
+    private String getExcelColumnName(int columnIndex) {
+        // 26一循环的次数
+        int columnCircleCount = columnIndex / 26;
+        // 26一循环内的位置
+        int thisCircleColumnIndex = columnIndex % 26;
+        // 26一循环的次数大于0,则视为栏名至少两位
+        String columnPrefix = columnCircleCount == 0
+            ? StrUtil.EMPTY
+            : StrUtil.subWithLength(EXCEL_COLUMN_NAME, columnCircleCount - 1, 1);
+        // 从26一循环内取对应的栏位名
+        String columnNext = StrUtil.subWithLength(EXCEL_COLUMN_NAME, thisCircleColumnIndex, 1);
+        // 将二者拼接即为最终的栏位名
+        return columnPrefix + columnNext;
+    }
+}

+ 69 - 14
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java

@@ -10,21 +10,19 @@ import com.alibaba.excel.write.metadata.WriteSheet;
 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 org.dromara.common.core.utils.StringUtils;
-import org.dromara.common.core.utils.file.FileUtils;
-import org.dromara.common.excel.convert.ExcelBigNumberConvert;
-import org.dromara.common.excel.core.CellMergeStrategy;
-import org.dromara.common.excel.core.DefaultExcelListener;
-import org.dromara.common.excel.core.ExcelListener;
-import org.dromara.common.excel.core.ExcelResult;
 import jakarta.servlet.ServletOutputStream;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.file.FileUtils;
+import org.dromara.common.excel.convert.ExcelBigNumberConvert;
+import org.dromara.common.excel.core.*;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -87,7 +85,26 @@ public class ExcelUtil {
         try {
             resetResponse(sheetName, response);
             ServletOutputStream os = response.getOutputStream();
-            exportExcel(list, sheetName, clazz, false, os);
+            exportExcel(list, sheetName, clazz, false, os, null);
+        } catch (IOException e) {
+            throw new RuntimeException("导出Excel异常");
+        }
+    }
+
+    /**
+     * 导出excel
+     *
+     * @param list      导出数据集合
+     * @param sheetName 工作表的名称
+     * @param clazz     实体类
+     * @param response  响应体
+     * @param options   级联下拉选
+     */
+    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response, List<DropDownOptions> options) {
+        try {
+            resetResponse(sheetName, response);
+            ServletOutputStream os = response.getOutputStream();
+            exportExcel(list, sheetName, clazz, false, os, options);
         } catch (IOException e) {
             throw new RuntimeException("导出Excel异常");
         }
@@ -106,7 +123,27 @@ public class ExcelUtil {
         try {
             resetResponse(sheetName, response);
             ServletOutputStream os = response.getOutputStream();
-            exportExcel(list, sheetName, clazz, merge, os);
+            exportExcel(list, sheetName, clazz, merge, os, null);
+        } catch (IOException e) {
+            throw new RuntimeException("导出Excel异常");
+        }
+    }
+
+    /**
+     * 导出excel
+     *
+     * @param list      导出数据集合
+     * @param sheetName 工作表的名称
+     * @param clazz     实体类
+     * @param merge     是否合并单元格
+     * @param response  响应体
+     * @param options   级联下拉选
+     */
+    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, HttpServletResponse response, List<DropDownOptions> options) {
+        try {
+            resetResponse(sheetName, response);
+            ServletOutputStream os = response.getOutputStream();
+            exportExcel(list, sheetName, clazz, merge, os, options);
         } catch (IOException e) {
             throw new RuntimeException("导出Excel异常");
         }
@@ -121,7 +158,20 @@ public class ExcelUtil {
      * @param os        输出流
      */
     public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os) {
-        exportExcel(list, sheetName, clazz, false, os);
+        exportExcel(list, sheetName, clazz, false, os, null);
+    }
+
+    /**
+     * 导出excel
+     *
+     * @param list      导出数据集合
+     * @param sheetName 工作表的名称
+     * @param clazz     实体类
+     * @param os        输出流
+     * @param options   级联下拉选内容
+     */
+    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os, List<DropDownOptions> options) {
+        exportExcel(list, sheetName, clazz, false, os, options);
     }
 
     /**
@@ -133,7 +183,8 @@ public class ExcelUtil {
      * @param merge     是否合并单元格
      * @param os        输出流
      */
-    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, OutputStream os) {
+    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge,
+                                       OutputStream os, List<DropDownOptions> options) {
         ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz)
             .autoCloseStream(false)
             // 自动适配
@@ -145,6 +196,10 @@ public class ExcelUtil {
             // 合并处理器
             builder.registerWriteHandler(new CellMergeStrategy(list, true));
         }
+        if (CollUtil.isNotEmpty(options)) {
+            // 添加下拉框操作
+            builder.registerWriteHandler(new ExcelDownHandler(options));
+        }
         builder.doWrite(list);
     }
 
@@ -253,7 +308,7 @@ public class ExcelUtil {
     /**
      * 重置响应体
      */
-    private static void resetResponse(String sheetName, HttpServletResponse response) {
+    private static void resetResponse(String sheetName, HttpServletResponse response) throws UnsupportedEncodingException {
         String filename = encodingFilename(sheetName);
         FileUtils.setAttachmentResponseHeader(response, filename);
         response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
@@ -275,7 +330,7 @@ public class ExcelUtil {
             if (StringUtils.containsAny(propertyValue, separator)) {
                 for (String value : propertyValue.split(separator)) {
                     if (itemArray[0].equals(value)) {
-                        propertyString.append(itemArray[1]).append(separator);
+                        propertyString.append(itemArray[1] + separator);
                         break;
                     }
                 }
@@ -304,7 +359,7 @@ public class ExcelUtil {
             if (StringUtils.containsAny(propertyValue, separator)) {
                 for (String value : propertyValue.split(separator)) {
                     if (itemArray[1].equals(value)) {
-                        propertyString.append(itemArray[0]).append(separator);
+                        propertyString.append(itemArray[0] + separator);
                         break;
                     }
                 }

+ 33 - 5
ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestExcelController.java

@@ -1,14 +1,19 @@
 package org.dromara.demo.controller;
 
 import cn.hutool.core.collection.CollUtil;
-import org.dromara.common.excel.utils.ExcelUtil;
+import jakarta.servlet.http.HttpServletResponse;
 import lombok.AllArgsConstructor;
 import lombok.Data;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.excel.core.ExcelResult;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.demo.domain.vo.ExportDemoVo;
+import org.dromara.demo.listener.ExportDemoListener;
+import org.dromara.demo.service.IExportExcelService;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
-import jakarta.servlet.http.HttpServletResponse;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -19,10 +24,13 @@ import java.util.Map;
  *
  * @author Lion Li
  */
+@RequiredArgsConstructor
 @RestController
 @RequestMapping("/demo/excel")
 public class TestExcelController {
 
+    private final IExportExcelService exportExcelService;
+
     /**
      * 单列表多数据
      */
@@ -76,6 +84,26 @@ public class TestExcelController {
         ExcelUtil.exportTemplateMultiList(multiListMap, "多列表.xlsx", "excel/多列表.xlsx", response);
     }
 
+    /**
+     * 导出下拉框
+     *
+     * @param response /
+     */
+    @GetMapping("/exportWithOptions")
+    public void exportWithOptions(HttpServletResponse response) {
+        exportExcelService.exportWithOptions(response);
+    }
+
+    /**
+     * 导入表格
+     */
+    @PostMapping(value = "/importWithOptions", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public List<ExportDemoVo> importWithOptions(@RequestPart("file") MultipartFile file) throws Exception {
+        // 处理解析结果
+        ExcelResult<ExportDemoVo> excelResult = ExcelUtil.importExcel(file.getInputStream(), ExportDemoVo.class, new ExportDemoListener());
+        return excelResult.getList();
+    }
+
     @Data
     @AllArgsConstructor
     static class TestObj1 {

+ 118 - 0
ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/vo/ExportDemoVo.java

@@ -0,0 +1,118 @@
+package org.dromara.demo.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.dromara.common.core.enums.UserStatus;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.excel.annotation.ExcelDictFormat;
+import org.dromara.common.excel.annotation.ExcelEnumFormat;
+import org.dromara.common.excel.convert.ExcelDictConvert;
+import org.dromara.common.excel.convert.ExcelEnumConvert;
+
+/**
+ * 带有下拉选的Excel导出
+ *
+ * @author Emil.Zhang
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AllArgsConstructor
+@NoArgsConstructor
+public class ExportDemoVo {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 用户昵称
+     */
+    @ExcelProperty(value = "用户名", index = 0)
+    @NotEmpty(message = "用户名不能为空", groups = AddGroup.class)
+    private String nickName;
+
+    /**
+     * 用户类型
+     * </p>
+     * 使用ExcelEnumFormat注解需要进行下拉选的部分
+     */
+    @ExcelProperty(value = "用户类型", index = 1, converter = ExcelEnumConvert.class)
+    @ExcelEnumFormat(enumClass = UserStatus.class, textField = "info")
+    @NotEmpty(message = "用户类型不能为空", groups = AddGroup.class)
+    private String userStatus;
+
+    /**
+     * 性别
+     * <p>
+     * 使用ExcelDictFormat注解需要进行下拉选的部分
+     */
+    @ExcelProperty(value = "性别", index = 2, converter = ExcelDictConvert.class)
+    @ExcelDictFormat(dictType = "sys_user_sex")
+    @NotEmpty(message = "性别不能为空", groups = AddGroup.class)
+    private String gender;
+
+    /**
+     * 手机号
+     */
+    @ExcelProperty(value = "手机号", index = 3)
+    @NotEmpty(message = "手机号不能为空", groups = AddGroup.class)
+    private String phoneNumber;
+
+    /**
+     * Email
+     */
+    @ExcelProperty(value = "Email", index = 4)
+    @NotEmpty(message = "Email不能为空", groups = AddGroup.class)
+    private String email;
+
+    /**
+     * 省
+     * <p>
+     * 级联下拉,仅判断是否选了
+     */
+    @ExcelProperty(value = "省", index = 5)
+    @NotNull(message = "省不能为空", groups = AddGroup.class)
+    private String province;
+
+    /**
+     * 数据库中的省ID
+     * </p>
+     * 处理完毕后再判断是否市正确的值
+     */
+    @NotNull(message = "请勿手动输入", groups = EditGroup.class)
+    private Integer provinceId;
+
+    /**
+     * 市
+     * <p>
+     * 级联下拉
+     */
+    @ExcelProperty(value = "市", index = 6)
+    @NotNull(message = "市不能为空", groups = AddGroup.class)
+    private String city;
+
+    /**
+     * 数据库中的市ID
+     */
+    @NotNull(message = "请勿手动输入", groups = EditGroup.class)
+    private Integer cityId;
+
+    /**
+     * 县
+     * <p>
+     * 级联下拉
+     */
+    @ExcelProperty(value = "县", index = 7)
+    @NotNull(message = "县不能为空", groups = AddGroup.class)
+    private String area;
+
+    /**
+     * 数据库中的县ID
+     */
+    @NotNull(message = "请勿手动输入", groups = EditGroup.class)
+    private Integer areaId;
+}

+ 68 - 0
ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/listener/ExportDemoListener.java

@@ -0,0 +1,68 @@
+package org.dromara.demo.listener;
+
+import cn.hutool.core.util.NumberUtil;
+import com.alibaba.excel.context.AnalysisContext;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.excel.core.DefaultExcelListener;
+import org.dromara.common.excel.core.DropDownOptions;
+import org.dromara.demo.domain.vo.ExportDemoVo;
+
+import java.util.List;
+
+/**
+ * Excel带下拉框的解析处理器
+ *
+ * @author Emil.Zhang
+ */
+public class ExportDemoListener extends DefaultExcelListener<ExportDemoVo> {
+
+    public ExportDemoListener() {
+        // 显示使用构造函数,否则将导致空指针
+        super(true);
+    }
+
+    @Override
+    public void invoke(ExportDemoVo data, AnalysisContext context) {
+        // 先校验必填
+        ValidatorUtils.validate(data, AddGroup.class);
+
+        // 处理级联下拉的部分
+        String province = data.getProvince();
+        String city = data.getCity();
+        String area = data.getArea();
+        // 本行用户选择的省
+        List<String> thisRowSelectedProvinceOption = DropDownOptions.analyzeOptionValue(province);
+        if (thisRowSelectedProvinceOption.size() == 2) {
+            String provinceIdStr = thisRowSelectedProvinceOption.get(1);
+            if (NumberUtil.isNumber(provinceIdStr)) {
+                // 严格要求数据的话可以在这里做与数据库相关的判断
+                // 例如判断省信息是否在数据库中存在等,建议结合RedisCache做缓存10s,减少数据库调用
+                data.setProvinceId(Integer.parseInt(provinceIdStr));
+            }
+        }
+        // 本行用户选择的市
+        List<String> thisRowSelectedCityOption = DropDownOptions.analyzeOptionValue(city);
+        if (thisRowSelectedCityOption.size() == 2) {
+            String cityIdStr = thisRowSelectedCityOption.get(1);
+            if (NumberUtil.isNumber(cityIdStr)) {
+                data.setCityId(Integer.parseInt(cityIdStr));
+            }
+        }
+        // 本行用户选择的县
+        List<String> thisRowSelectedAreaOption = DropDownOptions.analyzeOptionValue(area);
+        if (thisRowSelectedAreaOption.size() == 2) {
+            String areaIdStr = thisRowSelectedAreaOption.get(1);
+            if (NumberUtil.isNumber(areaIdStr)) {
+                data.setAreaId(Integer.parseInt(areaIdStr));
+            }
+        }
+
+        // 处理完毕以后判断是否符合规则
+        ValidatorUtils.validate(data, EditGroup.class);
+
+        // 添加到处理结果中
+        getExcelResult().getList().add(data);
+    }
+}

+ 18 - 0
ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/IExportExcelService.java

@@ -0,0 +1,18 @@
+package org.dromara.demo.service;
+
+import jakarta.servlet.http.HttpServletResponse;
+
+/**
+ * 导出下拉框Excel示例
+ *
+ * @author Emil.Zhang
+ */
+public interface IExportExcelService {
+
+    /**
+     * 导出下拉框
+     *
+     * @param response /
+     */
+    void exportWithOptions(HttpServletResponse response);
+}

+ 222 - 0
ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/impl/ExportExcelServiceImpl.java

@@ -0,0 +1,222 @@
+package org.dromara.demo.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.enums.UserStatus;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.excel.core.DropDownOptions;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.demo.domain.vo.ExportDemoVo;
+import org.dromara.demo.service.IExportExcelService;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 导出下拉框Excel示例
+ *
+ * @author Emil.Zhang
+ */
+@Service
+@RequiredArgsConstructor
+public class ExportExcelServiceImpl implements IExportExcelService {
+
+    @Override
+    public void exportWithOptions(HttpServletResponse response) {
+        // 创建表格数据,业务中一般通过数据库查询
+        List<ExportDemoVo> excelDataList = new ArrayList<>();
+        for (int i = 0; i < 3; i++) {
+            // 模拟数据库中的一条数据
+            ExportDemoVo everyRowData = new ExportDemoVo();
+            everyRowData.setNickName("用户-" + i);
+            everyRowData.setUserStatus(UserStatus.OK.getCode());
+            everyRowData.setGender("1");
+            everyRowData.setPhoneNumber(String.format("175%08d", i));
+            everyRowData.setEmail(String.format("175%08d", i) + "@163.com");
+            everyRowData.setProvinceId(i);
+            everyRowData.setCityId(i);
+            everyRowData.setAreaId(i);
+            excelDataList.add(everyRowData);
+        }
+
+        // 通过@ExcelIgnoreUnannotated配合@ExcelProperty合理显示需要的列
+        // 并通过@DropDown注解指定下拉值,或者通过创建ExcelOptions来指定下拉框
+        // 使用ExcelOptions时建议指定列index,防止出现下拉列解析不对齐
+
+        // 首先从数据库中查询下拉框内的可选项
+        // 这里模拟查询结果
+        List<DemoCityData> provinceList = getProvinceList(),
+            cityList = getCityList(provinceList),
+            areaList = getAreaList(cityList);
+        int provinceIndex = 5, cityIndex = 6, areaIndex = 7;
+
+        DropDownOptions provinceToCity = DropDownOptions.buildLinkedOptions(
+            provinceList,
+            provinceIndex,
+            cityList,
+            cityIndex,
+            DemoCityData::getId,
+            DemoCityData::getPid,
+            everyOptions -> DropDownOptions.createOptionValue(
+                everyOptions.getName(),
+                everyOptions.getId()
+            )
+        );
+
+        DropDownOptions cityToArea = DropDownOptions.buildLinkedOptions(
+            cityList,
+            cityIndex,
+            areaList,
+            areaIndex,
+            DemoCityData::getId,
+            DemoCityData::getPid,
+            everyOptions -> DropDownOptions.createOptionValue(
+                everyOptions.getName(),
+                everyOptions.getId()
+            )
+        );
+
+        // 把所有的下拉框存储
+        List<DropDownOptions> options = new ArrayList<>();
+        options.add(provinceToCity);
+        options.add(cityToArea);
+
+        // 到此为止所有的下拉框可选项已全部配置完毕
+
+        // 接下来需要将Excel中的展示数据转换为对应的下拉选
+        List<ExportDemoVo> outList = StreamUtils.toList(excelDataList, everyRowData -> {
+            // 只需要处理没有使用@ExcelDictFormat注解的下拉框
+            // 一般来说,可以直接在数据库查询即查询出省市县信息,这里通过模拟操作赋值
+            everyRowData.setProvince(buildOptions(provinceList, everyRowData.getProvinceId()));
+            everyRowData.setCity(buildOptions(cityList, everyRowData.getCityId()));
+            everyRowData.setArea(buildOptions(areaList, everyRowData.getAreaId()));
+            return everyRowData;
+        });
+
+        ExcelUtil.exportExcel(outList, "下拉框示例", ExportDemoVo.class, response, options);
+    }
+
+    private String buildOptions(List<DemoCityData> cityDataList, Integer id) {
+        Map<Integer, List<DemoCityData>> groupByIdMap =
+            cityDataList.stream().collect(Collectors.groupingBy(DemoCityData::getId));
+        if (groupByIdMap.containsKey(id)) {
+            DemoCityData demoCityData = groupByIdMap.get(id).get(0);
+            return DropDownOptions.createOptionValue(demoCityData.getName(), demoCityData.getId());
+        } else {
+            return StrUtil.EMPTY;
+        }
+    }
+
+    /**
+     * 模拟查询数据库操作
+     *
+     * @return /
+     */
+    private List<DemoCityData> getProvinceList() {
+        List<DemoCityData> provinceList = new ArrayList<>();
+
+        // 实际业务中一般采用数据库读取的形式,这里直接拼接创建
+        provinceList.add(new DemoCityData(0, null, "安徽省"));
+        provinceList.add(new DemoCityData(1, null, "江苏省"));
+
+        return provinceList;
+    }
+
+    /**
+     * 模拟查找数据库操作,需要连带查询出省的数据
+     *
+     * @param provinceList 模拟的父省数据
+     * @return /
+     */
+    private List<DemoCityData> getCityList(List<DemoCityData> provinceList) {
+        List<DemoCityData> cityList = new ArrayList<>();
+
+        // 实际业务中一般采用数据库读取的形式,这里直接拼接创建
+        cityList.add(new DemoCityData(0, 0, "合肥市"));
+        cityList.add(new DemoCityData(1, 0, "芜湖市"));
+        cityList.add(new DemoCityData(2, 1, "南京市"));
+        cityList.add(new DemoCityData(3, 1, "无锡市"));
+        cityList.add(new DemoCityData(4, 1, "徐州市"));
+
+        selectParentData(provinceList, cityList);
+
+        return cityList;
+    }
+
+    /**
+     * 模拟查找数据库操作,需要连带查询出市的数据
+     *
+     * @param cityList 模拟的父市数据
+     * @return /
+     */
+    private List<DemoCityData> getAreaList(List<DemoCityData> cityList) {
+        List<DemoCityData> areaList = new ArrayList<>();
+
+        // 实际业务中一般采用数据库读取的形式,这里直接拼接创建
+        areaList.add(new DemoCityData(0, 0, "瑶海区"));
+        areaList.add(new DemoCityData(1, 0, "庐江区"));
+        areaList.add(new DemoCityData(2, 1, "南宁县"));
+        areaList.add(new DemoCityData(3, 1, "镜湖区"));
+        areaList.add(new DemoCityData(4, 2, "玄武区"));
+        areaList.add(new DemoCityData(5, 2, "秦淮区"));
+        areaList.add(new DemoCityData(6, 3, "宜兴市"));
+        areaList.add(new DemoCityData(7, 3, "新吴区"));
+        areaList.add(new DemoCityData(8, 4, "鼓楼区"));
+        areaList.add(new DemoCityData(9, 4, "丰县"));
+
+        selectParentData(cityList, areaList);
+
+        return areaList;
+    }
+
+    /**
+     * 模拟数据库的查询父数据操作
+     *
+     * @param parentList /
+     * @param sonList    /
+     */
+    private void selectParentData(List<DemoCityData> parentList, List<DemoCityData> sonList) {
+        Map<Integer, List<DemoCityData>> parentGroupByIdMap =
+            parentList.stream().collect(Collectors.groupingBy(DemoCityData::getId));
+
+        sonList.forEach(everySon -> {
+            if (parentGroupByIdMap.containsKey(everySon.getPid())) {
+                everySon.setPData(parentGroupByIdMap.get(everySon.getPid()).get(0));
+            }
+        });
+    }
+
+    /**
+     * 模拟的数据库省市县
+     */
+    @Data
+    private static class DemoCityData {
+        /**
+         * 数据库id字段
+         */
+        private Integer id;
+        /**
+         * 数据库pid字段
+         */
+        private Integer pid;
+        /**
+         * 数据库name字段
+         */
+        private String name;
+        /**
+         * MyBatisPlus连带查询父数据
+         */
+        private DemoCityData pData;
+
+        public DemoCityData(Integer id, Integer pid, String name) {
+            this.id = id;
+            this.pid = pid;
+            this.name = name;
+        }
+    }
+}

+ 6 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDictTypeServiceImpl.java

@@ -265,4 +265,10 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
         }
     }
 
+    @Override
+    public Map<String, String> getAllDictByDictType(String dictType) {
+        List<SysDictDataVo> list = selectDictDataByType(dictType);
+        return StreamUtils.toMap(list, SysDictDataVo::getDictValue, SysDictDataVo::getDictLabel);
+    }
+
 }