init
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
package com.ruoyi.common.excel;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.metadata.Head;
|
||||
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
|
||||
import com.ruoyi.common.annotation.CellMerge;
|
||||
import com.ruoyi.common.utils.reflect.ReflectUtils;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 列值重复合并策略
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
public class CellMergeStrategy extends AbstractMergeStrategy {
|
||||
|
||||
private final List<CellRangeAddress> cellList;
|
||||
private final boolean hasTitle;
|
||||
private int rowIndex;
|
||||
|
||||
public CellMergeStrategy(List<?> list, boolean hasTitle) {
|
||||
this.hasTitle = hasTitle;
|
||||
// 行合并开始下标
|
||||
this.rowIndex = hasTitle ? 1 : 0;
|
||||
this.cellList = handle(list, hasTitle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
|
||||
// judge the list is not null
|
||||
if (CollUtil.isNotEmpty(cellList)) {
|
||||
// the judge is necessary
|
||||
if (cell.getRowIndex() == rowIndex && cell.getColumnIndex() == 0) {
|
||||
for (CellRangeAddress item : cellList) {
|
||||
sheet.addMergedRegion(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private List<CellRangeAddress> handle(List<?> list, boolean hasTitle) {
|
||||
List<CellRangeAddress> cellList = new ArrayList<>();
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return cellList;
|
||||
}
|
||||
Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName()));
|
||||
|
||||
// 有注解的字段
|
||||
List<Field> mergeFields = new ArrayList<>();
|
||||
List<Integer> mergeFieldsIndex = new ArrayList<>();
|
||||
for (int i = 0; i < fields.length; i++) {
|
||||
Field field = fields[i];
|
||||
if (field.isAnnotationPresent(CellMerge.class)) {
|
||||
CellMerge cm = field.getAnnotation(CellMerge.class);
|
||||
mergeFields.add(field);
|
||||
mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());
|
||||
if (hasTitle) {
|
||||
ExcelProperty property = field.getAnnotation(ExcelProperty.class);
|
||||
rowIndex = Math.max(rowIndex, property.value().length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<Field, RepeatCell> map = new HashMap<>();
|
||||
// 生成两两合并单元格
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
for (int j = 0; j < mergeFields.size(); j++) {
|
||||
Field field = mergeFields.get(j);
|
||||
Object val = ReflectUtils.invokeGetter(list.get(i), field.getName());
|
||||
|
||||
int colNum = mergeFieldsIndex.get(j);
|
||||
if (!map.containsKey(field)) {
|
||||
map.put(field, new RepeatCell(val, i));
|
||||
} else {
|
||||
RepeatCell repeatCell = map.get(field);
|
||||
Object cellValue = repeatCell.getValue();
|
||||
if (cellValue == null || "".equals(cellValue)) {
|
||||
// 空值跳过不合并
|
||||
continue;
|
||||
}
|
||||
if (!cellValue.equals(val)) {
|
||||
if (i - repeatCell.getCurrent() > 1) {
|
||||
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
|
||||
}
|
||||
map.put(field, new RepeatCell(val, i));
|
||||
} else if (i == list.size() - 1) {
|
||||
if (i > repeatCell.getCurrent()) {
|
||||
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cellList;
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
static class RepeatCell {
|
||||
|
||||
private Object value;
|
||||
|
||||
private int current;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.ruoyi.common.excel;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.excel.context.AnalysisContext;
|
||||
import com.alibaba.excel.event.AnalysisEventListener;
|
||||
import com.alibaba.excel.exception.ExcelAnalysisException;
|
||||
import com.alibaba.excel.exception.ExcelDataConvertException;
|
||||
import com.ruoyi.common.utils.JsonUtils;
|
||||
import com.ruoyi.common.utils.StreamUtils;
|
||||
import com.ruoyi.common.utils.ValidatorUtils;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.ConstraintViolationException;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Excel 导入监听
|
||||
*
|
||||
* @author Yjoioooo
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
@NoArgsConstructor
|
||||
public class DefaultExcelListener<T> extends AnalysisEventListener<T> implements ExcelListener<T> {
|
||||
|
||||
/**
|
||||
* 是否Validator检验,默认为是
|
||||
*/
|
||||
private Boolean isValidate = Boolean.TRUE;
|
||||
|
||||
/**
|
||||
* excel 表头数据
|
||||
*/
|
||||
private Map<Integer, String> headMap;
|
||||
|
||||
/**
|
||||
* 导入回执
|
||||
*/
|
||||
private ExcelResult<T> excelResult;
|
||||
|
||||
public DefaultExcelListener(boolean isValidate) {
|
||||
this.excelResult = new DefaultExcelResult<>();
|
||||
this.isValidate = isValidate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理异常
|
||||
*
|
||||
* @param exception ExcelDataConvertException
|
||||
* @param context Excel 上下文
|
||||
*/
|
||||
@Override
|
||||
public void onException(Exception exception, AnalysisContext context) throws Exception {
|
||||
String errMsg = null;
|
||||
if (exception instanceof ExcelDataConvertException) {
|
||||
// 如果是某一个单元格的转换异常 能获取到具体行号
|
||||
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;
|
||||
Integer rowIndex = excelDataConvertException.getRowIndex();
|
||||
Integer columnIndex = excelDataConvertException.getColumnIndex();
|
||||
errMsg = StrUtil.format("第{}行-第{}列-表头{}: 解析异常<br/>",
|
||||
rowIndex + 1, columnIndex + 1, headMap.get(columnIndex));
|
||||
if (log.isDebugEnabled()) {
|
||||
log.error(errMsg);
|
||||
}
|
||||
}
|
||||
if (exception instanceof ConstraintViolationException) {
|
||||
ConstraintViolationException constraintViolationException = (ConstraintViolationException) exception;
|
||||
Set<ConstraintViolation<?>> constraintViolations = constraintViolationException.getConstraintViolations();
|
||||
String constraintViolationsMsg = StreamUtils.join(constraintViolations, ConstraintViolation::getMessage, ", ");
|
||||
errMsg = StrUtil.format("第{}行数据校验异常: {}", context.readRowHolder().getRowIndex() + 1, constraintViolationsMsg);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.error(errMsg);
|
||||
}
|
||||
}
|
||||
excelResult.getErrorList().add(errMsg);
|
||||
throw new ExcelAnalysisException(errMsg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
|
||||
this.headMap = headMap;
|
||||
log.debug("解析到一条表头数据: {}", JsonUtils.toJsonString(headMap));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(T data, AnalysisContext context) {
|
||||
if (isValidate) {
|
||||
ValidatorUtils.validate(data);
|
||||
}
|
||||
excelResult.getList().add(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doAfterAllAnalysed(AnalysisContext context) {
|
||||
log.debug("所有数据解析完成!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExcelResult<T> getExcelResult() {
|
||||
return excelResult;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.ruoyi.common.excel;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 默认excel返回对象
|
||||
*
|
||||
* @author Yjoioooo
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class DefaultExcelResult<T> implements ExcelResult<T> {
|
||||
|
||||
/**
|
||||
* 数据对象list
|
||||
*/
|
||||
@Setter
|
||||
private List<T> list;
|
||||
|
||||
/**
|
||||
* 错误信息列表
|
||||
*/
|
||||
@Setter
|
||||
private List<String> errorList;
|
||||
|
||||
public DefaultExcelResult() {
|
||||
this.list = new ArrayList<>();
|
||||
this.errorList = new ArrayList<>();
|
||||
}
|
||||
|
||||
public DefaultExcelResult(List<T> list, List<String> errorList) {
|
||||
this.list = list;
|
||||
this.errorList = errorList;
|
||||
}
|
||||
|
||||
public DefaultExcelResult(ExcelResult<T> excelResult) {
|
||||
this.list = excelResult.getList();
|
||||
this.errorList = excelResult.getErrorList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<T> getList() {
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getErrorList() {
|
||||
return errorList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取导入回执
|
||||
*
|
||||
* @return 导入回执
|
||||
*/
|
||||
@Override
|
||||
public String getAnalysis() {
|
||||
int successCount = list.size();
|
||||
int errorCount = errorList.size();
|
||||
if (successCount == 0) {
|
||||
return "读取失败,未解析到数据";
|
||||
} else {
|
||||
if (errorCount == 0) {
|
||||
return StrUtil.format("恭喜您,全部读取成功!共{}条", successCount);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.ruoyi.common.excel;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
package com.ruoyi.common.excel;
|
||||
|
||||
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.metadata.FieldCache;
|
||||
import com.alibaba.excel.metadata.FieldWrapper;
|
||||
import com.alibaba.excel.util.ClassUtils;
|
||||
import com.alibaba.excel.write.handler.SheetWriteHandler;
|
||||
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
|
||||
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
|
||||
import com.ruoyi.common.annotation.ExcelDictFormat;
|
||||
import com.ruoyi.common.annotation.ExcelEnumFormat;
|
||||
import com.ruoyi.common.core.service.DictService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StreamUtils;
|
||||
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.ss.util.CellRangeAddressList;
|
||||
import org.apache.poi.ss.util.WorkbookUtil;
|
||||
import org.apache.poi.xssf.usermodel.XSSFDataValidation;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* <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();
|
||||
Workbook workbook = writeWorkbookHolder.getWorkbook();
|
||||
FieldCache fieldCache = ClassUtils.declaredFields(writeWorkbookHolder.getClazz(), writeWorkbookHolder);
|
||||
for (Map.Entry<Integer, FieldWrapper> entry : fieldCache.getSortedFieldMap().entrySet()) {
|
||||
Integer index = entry.getKey();
|
||||
FieldWrapper wrapper = entry.getValue();
|
||||
Field field = wrapper.getField();
|
||||
// 循环实体中的每个属性
|
||||
// 可选的下拉值
|
||||
List<String> options = new ArrayList<>();
|
||||
if (field.isAnnotationPresent(ExcelDictFormat.class)) {
|
||||
// 如果指定了@ExcelDictFormat,则使用字典的逻辑
|
||||
ExcelDictFormat format = field.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 (field.isAnnotationPresent(ExcelEnumFormat.class)) {
|
||||
// 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑
|
||||
ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class);
|
||||
List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
|
||||
options = StreamUtils.toList(values, String::valueOf);
|
||||
}
|
||||
if (ObjectUtil.isNotEmpty(options)) {
|
||||
// 仅当下拉可选项不为空时执行
|
||||
if (options.size() > 20) {
|
||||
// 这里限制如果可选项大于20,则使用额外表形式
|
||||
dropDownWithSheet(helper, workbook, sheet, index, options);
|
||||
} else {
|
||||
// 否则使用固定值形式
|
||||
dropDownWithSimple(helper, sheet, index, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (CollUtil.isEmpty(dropDownOptions)) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ruoyi.common.excel;
|
||||
|
||||
import com.alibaba.excel.read.listener.ReadListener;
|
||||
|
||||
/**
|
||||
* Excel 导入监听
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public interface ExcelListener<T> extends ReadListener<T> {
|
||||
|
||||
ExcelResult<T> getExcelResult();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.ruoyi.common.excel;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* excel返回对象
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public interface ExcelResult<T> {
|
||||
|
||||
/**
|
||||
* 对象列表
|
||||
*/
|
||||
List<T> getList();
|
||||
|
||||
/**
|
||||
* 错误列表
|
||||
*/
|
||||
List<String> getErrorList();
|
||||
|
||||
/**
|
||||
* 导入回执
|
||||
*/
|
||||
String getAnalysis();
|
||||
}
|
||||
Reference in New Issue
Block a user