From 4a9aafe8530e945bd52d8093944c4bcb1a89c28f Mon Sep 17 00:00:00 2001
From: 疯狂的狮子Li <15040126243@163.com>
Date: 星期日, 25 六月 2023 11:13:05 +0800
Subject: [PATCH] add 增加 excel 导出下拉框功能

---
 ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestExcelController.java          |   38 +
 ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/impl/ExportExcelServiceImpl.java     |  222 +++++++++++
 ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDictTypeServiceImpl.java |    6 
 ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java          |   83 +++
 ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java    |  370 +++++++++++++++++++
 ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/IExportExcelService.java             |   18 
 ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/vo/ExportDemoVo.java                  |  118 ++++++
 ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DropDownOptions.java     |  149 +++++++
 ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/listener/ExportDemoListener.java             |   68 +++
 ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DictService.java        |   10 
 10 files changed, 1,063 insertions(+), 19 deletions(-)

diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DictService.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DictService.java
index 04d0f9b..b78a7f2 100644
--- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/service/DictService.java
+++ b/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 @@
      */
     String getDictValue(String dictType, String dictLabel, String separator);
 
+    /**
+     * 鑾峰彇瀛楀吀涓嬫墍鏈夌殑瀛楀吀鍊间笌鏍囩
+     *
+     * @param dictType 瀛楀吀绫诲瀷
+     * @return dictValue涓簁ey锛宒ictLabel涓哄�肩粍鎴愮殑Map
+     */
+    Map<String, String> getAllDictByDictType(String dictType);
+
 }
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DropDownOptions.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/DropDownOptions.java
new file mode 100644
index 0000000..8b53a0c
--- /dev/null
+++ b/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>
+ * 娉ㄦ剰锛氫负纭繚涓嬫媺妗嗚В鏋愭纭紝浼犲�煎姟蹇呬娇鐢╟reateOptionValue()鍋氫负鍊肩殑鎷兼帴
+ *
+ * @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<>();
+    /**
+     * 浜岀骇涓嬫媺鎵�鍖呭惈鐨勬暟鎹甅ap
+     * <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<>();
+        // 鐖剁骇渚濇嵁鑷繁鐨処D鍒嗙粍
+        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);
+                // 鎻愬彇鍚嶇О鍜孖D浣滀负Key
+                String key = howToBuildEveryOption.apply(parentObj);
+                // Key瀵瑰簲鐨刅alue
+                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;
+    }
+}
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/core/ExcelDownHandler.java
new file mode 100644
index 0000000..52bfebc
--- /dev/null
+++ b/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>
+ * 鑰冭檻鍒颁笅鎷夐�夎繃澶氬彲鑳藉鑷碋xcel鎵撳紑缂撴參鐨勯棶棰橈紝鍙牎楠屽墠1000琛�
+ * <p>
+ * 鍗冲彧鏈夊墠1000琛岀殑鏁版嵁鍙互鐢ㄤ笅鎷夋锛岃秴鍑虹殑鑷閫氳繃闄愬埗鏁版嵁閲忕殑褰㈠紡锛岀浜屾杈撳嚭
+ *
+ * @author Emil.Zhang
+ */
+@Slf4j
+public class ExcelDownHandler implements SheetWriteHandler {
+
+    /**
+     * Excel琛ㄦ牸涓殑鍒楀悕鑻辨枃
+     * 浠呬负浜嗚В鏋愬垪鑻辨枃锛岀姝慨鏀�
+     */
+    private static final String EXCEL_COLUMN_NAME = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+    /**
+     * 鍗曢�夋暟鎹甋heet鍚�
+     */
+    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.閫氳繃瑙f瀽浼犲叆鐨凘ExcelProperty鍚岀骇鏄惁鏍囨敞鏈堾DropDown閫夐」
+     * 濡傛灉鏈変笖璁剧疆浜唙alue鍊硷紝鍒欏皢鍏剁洿鎺ョ疆涓轰笅鎷夊彲閫夐」
+     * <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 鍒梚ndex
+     * @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瑙f瀽閿欒
+                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);
+            // 鏁版嵁楠岃瘉涓哄簭鍒楁ā寮忥紝寮曠敤鍒版瘡涓�涓富琛ㄤ腑鐨勪簩绾ч�夐」浣嶇疆
+            // 鍒涘缓瀛愰」鐨勫悕绉扮鐞嗗櫒锛屽彧鏄负浜嗕娇寰桬xcel鍙互璇嗗埆鍒版暟鎹�
+            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>
+     * 鐢变簬涓嬫媺妗嗗彲閫夊�兼暟閲忚繃澶氾紝涓烘彁鍗嘐xcel鎵撳紑鏁堢巼锛屼娇鐢ㄩ澶栬〃鏍煎舰寮忓仛涓嬫媺
+     *
+     * @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>渚濇嵁鍒梚ndex鑾峰彇鍒楀悕鑻辨枃</h2>
+     * 渚濇嵁鍒梚ndex杞崲涓篍xcel涓殑鍒楀悕鑻辨枃
+     * <p>渚嬪绗�1鍒楋紝index涓�0锛岃В鏋愬嚭鏉ヤ负A鍒�</p>
+     * 绗�27鍒楋紝index涓�26锛岃В鏋愪负AA鍒�
+     * <p>绗�28鍒楋紝index涓�27锛岃В鏋愪负AB鍒�</p>
+     *
+     * @param columnIndex 鍒梚ndex
+     * @return 鍒梚ndex鎵�鍦ㄥ緱鑻辨枃鍚�
+     */
+    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;
+    }
+}
diff --git a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java b/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java
index 3605cf2..00a78ea 100644
--- a/ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java
+++ b/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.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 @@
         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 @@
         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 @@
      * @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 @@
      * @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)
             // 鑷姩閫傞厤
@@ -144,6 +195,10 @@
         if (merge) {
             // 鍚堝苟澶勭悊鍣�
             builder.registerWriteHandler(new CellMergeStrategy(list, true));
+        }
+        if (CollUtil.isNotEmpty(options)) {
+            // 娣诲姞涓嬫媺妗嗘搷浣�
+            builder.registerWriteHandler(new ExcelDownHandler(options));
         }
         builder.doWrite(list);
     }
@@ -253,7 +308,7 @@
     /**
      * 閲嶇疆鍝嶅簲浣�
      */
-    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 @@
             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 @@
             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;
                     }
                 }
diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestExcelController.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestExcelController.java
index d83455d..418f740 100644
--- a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestExcelController.java
+++ b/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,9 +24,12 @@
  *
  * @author Lion Li
  */
+@RequiredArgsConstructor
 @RestController
 @RequestMapping("/demo/excel")
 public class TestExcelController {
+
+    private final IExportExcelService exportExcelService;
 
     /**
      * 鍗曞垪琛ㄥ鏁版嵁
@@ -76,6 +84,26 @@
         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 {
+        // 澶勭悊瑙f瀽缁撴灉
+        ExcelResult<ExportDemoVo> excelResult = ExcelUtil.importExcel(file.getInputStream(), ExportDemoVo.class, new ExportDemoListener());
+        return excelResult.getList();
+    }
+
     @Data
     @AllArgsConstructor
     static class TestObj1 {
diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/vo/ExportDemoVo.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/vo/ExportDemoVo.java
new file mode 100644
index 0000000..95fa0d1
--- /dev/null
+++ b/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;
+}
diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/listener/ExportDemoListener.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/listener/ExportDemoListener.java
new file mode 100644
index 0000000..7bd4e1e
--- /dev/null
+++ b/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);
+    }
+}
diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/IExportExcelService.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/IExportExcelService.java
new file mode 100644
index 0000000..4dfa5ef
--- /dev/null
+++ b/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;
+
+/**
+ * 瀵煎嚭涓嬫媺妗咵xcel绀轰緥
+ *
+ * @author Emil.Zhang
+ */
+public interface IExportExcelService {
+
+    /**
+     * 瀵煎嚭涓嬫媺妗�
+     *
+     * @param response /
+     */
+    void exportWithOptions(HttpServletResponse response);
+}
diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/impl/ExportExcelServiceImpl.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/service/impl/ExportExcelServiceImpl.java
new file mode 100644
index 0000000..0240e02
--- /dev/null
+++ b/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;
+
+/**
+ * 瀵煎嚭涓嬫媺妗咵xcel绀轰緥
+ *
+ * @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锛岄槻姝㈠嚭鐜颁笅鎷夊垪瑙f瀽涓嶅榻�
+
+        // 棣栧厛浠庢暟鎹簱涓煡璇笅鎷夋鍐呯殑鍙�夐」
+        // 杩欓噷妯℃嫙鏌ヨ缁撴灉
+        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 {
+        /**
+         * 鏁版嵁搴搃d瀛楁
+         */
+        private Integer id;
+        /**
+         * 鏁版嵁搴損id瀛楁
+         */
+        private Integer pid;
+        /**
+         * 鏁版嵁搴搉ame瀛楁
+         */
+        private String name;
+        /**
+         * MyBatisPlus杩炲甫鏌ヨ鐖舵暟鎹�
+         */
+        private DemoCityData pData;
+
+        public DemoCityData(Integer id, Integer pid, String name) {
+            this.id = id;
+            this.pid = pid;
+            this.name = name;
+        }
+    }
+}
diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDictTypeServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDictTypeServiceImpl.java
index 60431ec..971b0b2 100644
--- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDictTypeServiceImpl.java
+++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDictTypeServiceImpl.java
@@ -265,4 +265,10 @@
         }
     }
 
+    @Override
+    public Map<String, String> getAllDictByDictType(String dictType) {
+        List<SysDictDataVo> list = selectDictDataByType(dictType);
+        return StreamUtils.toMap(list, SysDictDataVo::getDictValue, SysDictDataVo::getDictLabel);
+    }
+
 }

--
Gitblit v1.9.3