ExcelDownHandler.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. package com.ruoyi.common.excel;
  2. import cn.hutool.core.util.EnumUtil;
  3. import cn.hutool.core.util.ObjectUtil;
  4. import cn.hutool.core.util.StrUtil;
  5. import com.alibaba.excel.annotation.ExcelProperty;
  6. import com.alibaba.excel.write.handler.SheetWriteHandler;
  7. import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
  8. import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
  9. import com.ruoyi.common.annotation.ExcelDictFormat;
  10. import com.ruoyi.common.annotation.ExcelEnumFormat;
  11. import com.ruoyi.common.core.service.DictService;
  12. import com.ruoyi.common.exception.ServiceException;
  13. import com.ruoyi.common.utils.spring.SpringUtils;
  14. import lombok.extern.slf4j.Slf4j;
  15. import org.apache.poi.ss.usermodel.*;
  16. import org.apache.poi.ss.util.CellRangeAddressList;
  17. import org.apache.poi.ss.util.WorkbookUtil;
  18. import org.apache.poi.xssf.usermodel.XSSFDataValidation;
  19. import java.lang.reflect.Field;
  20. import java.util.*;
  21. import java.util.stream.Collectors;
  22. /**
  23. * <h1>Excel表格下拉选操作</h1>
  24. * 考虑到下拉选过多可能导致Excel打开缓慢的问题,只校验前1000行
  25. * <p>
  26. * 即只有前1000行的数据可以用下拉框,超出的自行通过限制数据量的形式,第二次输出
  27. *
  28. * @author Emil.Zhang
  29. */
  30. @Slf4j
  31. public class ExcelDownHandler implements SheetWriteHandler {
  32. /**
  33. * Excel表格中的列名英文
  34. * 仅为了解析列英文,禁止修改
  35. */
  36. private static final String EXCEL_COLUMN_NAME = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  37. /**
  38. * 单选数据Sheet名
  39. */
  40. private static final String OPTIONS_SHEET_NAME = "options";
  41. /**
  42. * 联动选择数据Sheet名的头
  43. */
  44. private static final String LINKED_OPTIONS_SHEET_NAME = "linkedOptions";
  45. /**
  46. * 下拉可选项
  47. */
  48. private final List<DropDownOptions> dropDownOptions;
  49. /**
  50. * 当前单选进度
  51. */
  52. private int currentOptionsColumnIndex;
  53. /**
  54. * 当前联动选择进度
  55. */
  56. private int currentLinkedOptionsSheetIndex;
  57. private final DictService dictService;
  58. public ExcelDownHandler(List<DropDownOptions> options) {
  59. this.dropDownOptions = options;
  60. this.currentOptionsColumnIndex = 0;
  61. this.currentLinkedOptionsSheetIndex = 0;
  62. this.dictService = SpringUtils.getBean(DictService.class);
  63. }
  64. /**
  65. * <h2>开始创建下拉数据</h2>
  66. * 1.通过解析传入的@ExcelProperty同级是否标注有@DropDown选项
  67. * 如果有且设置了value值,则将其直接置为下拉可选项
  68. * <p>
  69. * 2.或者在调用ExcelUtil时指定了可选项,将依据传入的可选项做下拉
  70. * <p>
  71. * 3.二者并存,注意调用方式
  72. */
  73. @Override
  74. public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
  75. Sheet sheet = writeSheetHolder.getSheet();
  76. // 开始设置下拉框 HSSFWorkbook
  77. DataValidationHelper helper = sheet.getDataValidationHelper();
  78. Field[] fields = writeWorkbookHolder.getClazz().getDeclaredFields();
  79. Workbook workbook = writeWorkbookHolder.getWorkbook();
  80. int length = fields.length;
  81. for (int i = 0; i < length; i++) {
  82. // 循环实体中的每个属性
  83. // 可选的下拉值
  84. List<String> options = new ArrayList<>();
  85. if (fields[i].isAnnotationPresent(ExcelDictFormat.class)) {
  86. // 如果指定了@ExcelDictFormat,则使用字典的逻辑
  87. ExcelDictFormat thisFiledExcelDictFormat = fields[i].getDeclaredAnnotation(ExcelDictFormat.class);
  88. String dictType = thisFiledExcelDictFormat.dictType();
  89. String converterExp = thisFiledExcelDictFormat.readConverterExp();
  90. if (StrUtil.isNotBlank(dictType)) {
  91. // 如果传递了字典名,则依据字典建立下拉
  92. options =
  93. new ArrayList<>(
  94. Optional.ofNullable(dictService.getAllDictByDictType(dictType))
  95. .orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
  96. .values()
  97. );
  98. } else if (StrUtil.isNotBlank(converterExp)) {
  99. // 如果指定了确切的值,则直接解析确切的值
  100. options = StrUtil.split(
  101. converterExp,
  102. thisFiledExcelDictFormat.separator(),
  103. true,
  104. true);
  105. }
  106. } else if (fields[i].isAnnotationPresent(ExcelEnumFormat.class)) {
  107. // 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑
  108. ExcelEnumFormat thisFiledExcelEnumFormat = fields[i].getDeclaredAnnotation(ExcelEnumFormat.class);
  109. options =
  110. EnumUtil
  111. .getFieldValues(
  112. thisFiledExcelEnumFormat.enumClass(),
  113. thisFiledExcelEnumFormat.textField()
  114. )
  115. .stream()
  116. .map(String::valueOf)
  117. .collect(Collectors.toList());
  118. }
  119. if (ObjectUtil.isNotEmpty(options)) {
  120. // 仅当下拉可选项不为空时执行
  121. // 获取列下标,默认为当前循环次数
  122. int index = i;
  123. if (fields[i].isAnnotationPresent(ExcelProperty.class)) {
  124. // 如果指定了列下标,以指定的为主
  125. index = fields[i].getDeclaredAnnotation(ExcelProperty.class).index();
  126. }
  127. if (options.size() > 20) {
  128. // 这里限制如果可选项大于20,则使用额外表形式
  129. dropDownWithSheet(helper, workbook, sheet, index, options);
  130. } else {
  131. // 否则使用固定值形式
  132. dropDownWithSimple(helper, sheet, index, options);
  133. }
  134. }
  135. }
  136. dropDownOptions.forEach(everyOptions -> {
  137. // 如果传递了下拉框选择器参数
  138. if (!everyOptions.getNextOptions().isEmpty()) {
  139. // 当二级选项不为空时,使用额外关联表的形式
  140. dropDownLinkedOptions(helper, workbook, sheet, everyOptions);
  141. } else if (everyOptions.getOptions().size() > 10) {
  142. // 当一级选项参数个数大于10,使用额外表的形式
  143. dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions());
  144. } else if (everyOptions.getOptions().size() != 0) {
  145. // 当一级选项个数不为空,使用默认形式
  146. dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions());
  147. }
  148. });
  149. }
  150. /**
  151. * <h2>简单下拉框</h2>
  152. * 直接将可选项拼接为指定列的数据校验值
  153. *
  154. * @param celIndex 列index
  155. * @param value 下拉选可选值
  156. */
  157. private void dropDownWithSimple(DataValidationHelper helper, Sheet sheet, Integer celIndex, List<String> value) {
  158. if (ObjectUtil.isEmpty(value)) {
  159. return;
  160. }
  161. this.markOptionsToSheet(helper, sheet, celIndex, helper.createExplicitListConstraint(value.toArray(new String[0])));
  162. }
  163. /**
  164. * <h2>额外表格形式的级联下拉框</h2>
  165. *
  166. * @param options 额外表格形式存储的下拉可选项
  167. */
  168. private void dropDownLinkedOptions(DataValidationHelper helper, Workbook workbook, Sheet sheet, DropDownOptions options) {
  169. String linkedOptionsSheetName = String.format("%s_%d", LINKED_OPTIONS_SHEET_NAME, currentLinkedOptionsSheetIndex);
  170. // 创建联动下拉数据表
  171. Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName));
  172. // 将下拉表隐藏
  173. workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true);
  174. // 完善横向的一级选项数据表
  175. List<String> firstOptions = options.getOptions();
  176. Map<String, List<String>> secoundOptionsMap = options.getNextOptions();
  177. // 创建名称管理器
  178. Name name = workbook.createName();
  179. // 设置名称管理器的别名
  180. name.setNameName(linkedOptionsSheetName);
  181. // 以横向第一行创建一级下拉拼接引用位置
  182. String firstOptionsFunction = String.format("%s!$%s$1:$%s$1",
  183. linkedOptionsSheetName,
  184. getExcelColumnName(0),
  185. getExcelColumnName(firstOptions.size())
  186. );
  187. // 设置名称管理器的引用位置
  188. name.setRefersToFormula(firstOptionsFunction);
  189. // 设置数据校验为序列模式,引用的是名称管理器中的别名
  190. this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName));
  191. for (int columIndex = 0; columIndex < firstOptions.size(); columIndex++) {
  192. // 先提取主表中一级下拉的列名
  193. String firstOptionsColumnName = getExcelColumnName(columIndex);
  194. // 一次循环是每一个一级选项
  195. int finalI = columIndex;
  196. // 本次循环的一级选项值
  197. String thisFirstOptionsValue = firstOptions.get(columIndex);
  198. // 创建第一行的数据
  199. Optional
  200. // 获取第一行
  201. .ofNullable(linkedOptionsDataSheet.getRow(0))
  202. // 如果不存在则创建第一行
  203. .orElseGet(() -> linkedOptionsDataSheet.createRow(finalI))
  204. // 第一行当前列
  205. .createCell(columIndex)
  206. // 设置值为当前一级选项值
  207. .setCellValue(thisFirstOptionsValue);
  208. // 第二行开始,设置第二级别选项参数
  209. List<String> secondOptions = secoundOptionsMap.get(thisFirstOptionsValue);
  210. if (ObjectUtil.isEmpty(secondOptions)) {
  211. // 必须保证至少有一个关联选项,否则将导致Excel解析错误
  212. secondOptions = Collections.singletonList("暂无_0");
  213. }
  214. // 以该一级选项值创建子名称管理器
  215. Name sonName = workbook.createName();
  216. // 设置名称管理器的别名
  217. sonName.setNameName(thisFirstOptionsValue);
  218. // 以第二行该列数据拼接引用位置
  219. String sonFunction = String.format("%s!$%s$2:$%s$%d",
  220. linkedOptionsSheetName,
  221. firstOptionsColumnName,
  222. firstOptionsColumnName,
  223. secondOptions.size() + 1
  224. );
  225. // 设置名称管理器的引用位置
  226. sonName.setRefersToFormula(sonFunction);
  227. // 数据验证为序列模式,引用到每一个主表中的二级选项位置
  228. // 创建子项的名称管理器,只是为了使得Excel可以识别到数据
  229. String mainSheetFirstOptionsColumnName = getExcelColumnName(options.getIndex());
  230. for (int i = 0; i < 100; i++) {
  231. // 以一级选项对应的主体所在位置创建二级下拉
  232. String secondOptionsFunction = String.format("=INDIRECT(%s%d)", mainSheetFirstOptionsColumnName, i + 1);
  233. // 二级只能主表每一行的每一列添加二级校验
  234. markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction));
  235. }
  236. for (int rowIndex = 0; rowIndex < secondOptions.size(); rowIndex++) {
  237. // 从第二行开始填充二级选项
  238. int finalRowIndex = rowIndex + 1;
  239. int finalColumIndex = columIndex;
  240. Row row = Optional
  241. .ofNullable(linkedOptionsDataSheet.getRow(finalRowIndex))
  242. // 没有则创建
  243. .orElseGet(() -> linkedOptionsDataSheet.createRow(finalRowIndex));
  244. Optional
  245. // 在本级一级选项所在的列
  246. .ofNullable(row.getCell(finalColumIndex))
  247. // 不存在则创建
  248. .orElseGet(() -> row.createCell(finalColumIndex))
  249. // 设置二级选项值
  250. .setCellValue(secondOptions.get(rowIndex));
  251. }
  252. }
  253. currentLinkedOptionsSheetIndex++;
  254. }
  255. /**
  256. * <h2>额外表格形式的普通下拉框</h2>
  257. * 由于下拉框可选值数量过多,为提升Excel打开效率,使用额外表格形式做下拉
  258. *
  259. * @param celIndex 下拉选
  260. * @param value 下拉选可选值
  261. */
  262. private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List<String> value) {
  263. // 创建下拉数据表
  264. Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)))
  265. .orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)));
  266. // 将下拉表隐藏
  267. workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true);
  268. // 完善纵向的一级选项数据表
  269. for (int i = 0; i < value.size(); i++) {
  270. int finalI = i;
  271. // 获取每一选项行,如果没有则创建
  272. Row row = Optional.ofNullable(simpleDataSheet.getRow(i))
  273. .orElseGet(() -> simpleDataSheet.createRow(finalI));
  274. // 获取本级选项对应的选项列,如果没有则创建
  275. Cell cell = Optional.ofNullable(row.getCell(currentOptionsColumnIndex))
  276. .orElseGet(() -> row.createCell(currentOptionsColumnIndex));
  277. // 设置值
  278. cell.setCellValue(value.get(i));
  279. }
  280. // 创建名称管理器
  281. Name name = workbook.createName();
  282. // 设置名称管理器的别名
  283. String nameName = String.format("%s_%d", OPTIONS_SHEET_NAME, celIndex);
  284. name.setNameName(nameName);
  285. // 以纵向第一列创建一级下拉拼接引用位置
  286. String function = String.format("%s!$%s$1:$%s$%d",
  287. OPTIONS_SHEET_NAME,
  288. getExcelColumnName(currentOptionsColumnIndex),
  289. getExcelColumnName(currentOptionsColumnIndex),
  290. value.size());
  291. // 设置名称管理器的引用位置
  292. name.setRefersToFormula(function);
  293. // 设置数据校验为序列模式,引用的是名称管理器中的别名
  294. this.markOptionsToSheet(helper, sheet, celIndex, helper.createFormulaListConstraint(nameName));
  295. currentOptionsColumnIndex++;
  296. }
  297. /**
  298. * 挂载下拉的列,仅限一级选项
  299. */
  300. private void markOptionsToSheet(DataValidationHelper helper,
  301. Sheet sheet,
  302. Integer celIndex,
  303. DataValidationConstraint constraint) {
  304. // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
  305. CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, celIndex, celIndex);
  306. markDataValidationToSheet(helper, sheet, constraint, addressList);
  307. }
  308. /**
  309. * 挂载下拉的列,仅限二级选项
  310. */
  311. private void markLinkedOptionsToSheet(DataValidationHelper helper,
  312. Sheet sheet,
  313. Integer rowIndex,
  314. Integer celIndex,
  315. DataValidationConstraint constraint) {
  316. // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
  317. CellRangeAddressList addressList = new CellRangeAddressList(rowIndex, rowIndex, celIndex, celIndex);
  318. markDataValidationToSheet(helper, sheet, constraint, addressList);
  319. }
  320. /**
  321. * 应用数据校验
  322. */
  323. private void markDataValidationToSheet(DataValidationHelper helper,
  324. Sheet sheet,
  325. DataValidationConstraint constraint,
  326. CellRangeAddressList addressList) {
  327. // 数据有效性对象
  328. DataValidation dataValidation = helper.createValidation(constraint, addressList);
  329. // 处理Excel兼容性问题
  330. if (dataValidation instanceof XSSFDataValidation) {
  331. //数据校验
  332. dataValidation.setSuppressDropDownArrow(true);
  333. //错误提示
  334. dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
  335. dataValidation.createErrorBox("提示", "此值与单元格定义数据不一致");
  336. dataValidation.setShowErrorBox(true);
  337. //选定提示
  338. dataValidation.createPromptBox("填写说明:", "填写内容只能为下拉中数据,其他数据将导致导入失败");
  339. dataValidation.setShowPromptBox(true);
  340. sheet.addValidationData(dataValidation);
  341. } else {
  342. dataValidation.setSuppressDropDownArrow(false);
  343. }
  344. sheet.addValidationData(dataValidation);
  345. }
  346. /**
  347. * <h2>依据列index获取列名英文</h2>
  348. * 依据列index转换为Excel中的列名英文
  349. * <p>例如第1列,index为0,解析出来为A列</p>
  350. * 第27列,index为26,解析为AA列
  351. * <p>第28列,index为27,解析为AB列</p>
  352. *
  353. * @param columnIndex 列index
  354. * @return 列index所在得英文名
  355. */
  356. private String getExcelColumnName(int columnIndex) {
  357. // 26一循环的次数
  358. int columnCircleCount = columnIndex / 26;
  359. // 26一循环内的位置
  360. int thisCircleColumnIndex = columnIndex % 26;
  361. // 26一循环的次数大于0,则视为栏名至少两位
  362. String columnPrefix = columnCircleCount == 0
  363. ? ""
  364. : StrUtil.subWithLength(EXCEL_COLUMN_NAME, columnCircleCount - 1, 1);
  365. // 从26一循环内取对应的栏位名
  366. String columnNext = StrUtil.subWithLength(EXCEL_COLUMN_NAME, thisCircleColumnIndex, 1);
  367. // 将二者拼接即为最终的栏位名
  368. return columnPrefix + columnNext;
  369. }
  370. }