昨天刚接了一个导入导出的需求,前端页面有三个下拉选择框有相对应的关联关系。在生成模板的时候也需要把这种关系给设置出来。 最终实现的效果为 A D E三列的数据能够实现级联效果,
- A列下拉选择之后,根据选择的内容是否包含特定条件,给D设置不同的数据有效性。
- D列选择之后,匹配对应的id给X列设置值,
- E列的数据有效性根据X列的值匹配对应的数据
- Y列的值根据E列的值匹配对应的Code
1. 具体的实现思路
- 通过隐藏Sheet页将需要用到的数据写入
- 通过名称管理器设置数据引用
- 通过几组特定的函数实现功能
| 函数 | 作用 |
|---|---|
| IF(ISNUMBER(FIND("中介",A{})),hsAgencyNames,CompanyNames) | 从A列判断是否包含特定字符串,然后设置对应的 名称管理器引用 |
| IFERROR(VLOOKUP($D{}, CompanyIdMap, 2, FALSE), "") | 根据D列的值,去CompanyIdMap这个名称引用中查询对应的ID |
| CompanyData!A2:A:100 | 创建名称管理器的引用范围 |
2. 拆分实现
1. 导出模板功能
java
public void downloadTemplateV2(HttpServletResponse response) throws Exception {
// 1. 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("级联导入模板", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 从自己的数据库获取数据
Map<String, CompanyVo> seaCmpNameIdMap = getCompanyMap();
// 根据自己的业务调整
Map<String, List<Org>> orgMap = new HashMap();
String topTipStr = "导入提示信息";
try (OutputStream out = response.getOutputStream()) {
// 2. 创建ExcelWriter
ExcelWriter writer = ExcelUtil.getWriter(true); // true表示创建xlsx格式
Sheet sheet = writer.getSheet();
Workbook workbook = writer.getWorkbook();
// 写入映射数据到工作表并创建名称管理器
writeCompanyAndOrgData(workbook, seaCmpNameIdMap, orgMap);
// 设置级联下拉和对应列填充
setCompanyValidationAndFormula(sheet, workbook);
setDeptValidationAndFormula(sheet, workbook);
List<String> headers = Arrays.asList(
"表头1","表头2","表头3","表头4"
);
writer.merge(0, 0, 0, 24, topTipStr, true);
writer.passRows(1);
writer.writeHeadRow(headers);
// 日期类型限制
restrictCell2DateFormat(sheet, 1);;
// 字典项类型的数据下拉设置
setDictDataValidation(sheet, "spiChecktypeHs", 0);
for (int i = 0; i < headers.size(); i++) {
String headerText = headers.get(i);
// 计算宽度:文字长度 × 2 × 256(Excel的宽度单位)
int columnWidth = headerText.length() * 2 * 256;
// 设置最小宽度(避免过窄)
columnWidth = Math.max(columnWidth, 8 * 256);
// 设置列宽
sheet.setColumnWidth(i, columnWidth);
}
writer.flush(out).close();
} catch (Exception e) {
log.info("下载模板失败", e);
}
}
2.写入映射数据和创建名称管理器
java
private void writeCompanyAndOrgData(Workbook workbook, Map<String, HsCompanyVo> seaCmpNameIdMap,
Map<String, List<Org>> orgMap) {
// ========== 1. 写入企业数据 ==========
Sheet companySheet = workbook.createSheet("CompanyData");
workbook.setSheetHidden(workbook.getSheetIndex(companySheet), true);
// 企业数据表头
Row companyHeader = companySheet.createRow(0);
companyHeader.createCell(0).setCellValue("企业名称");
companyHeader.createCell(1).setCellValue("企业ID");
int companyRow = 1;
for (Map.Entry<String, HsCompanyVo> entry : seaCmpNameIdMap.entrySet()) {
Row row = companySheet.createRow(companyRow++);
row.createCell(0).setCellValue(entry.getKey());
row.createCell(1).setCellValue(entry.getValue().getId());
}
// 创建企业名称列表和ID映射的名称管理器
// 这里我遇到了一个问题, 就是第二个参数也就是名称管理器的名字 必须以_或者 字母开头
createName(workbook, "CompanyNames", "CompanyData!$A$2:$A$" + companyRow);
//
createName(workbook, "CompanyIdMap", "CompanyData!$A$2:$B$" + companyRow);
Sheet orgSheet = workbook.createSheet("OrgData");
workbook.setSheetHidden(workbook.getSheetIndex(orgSheet), true);
// 单位数据表头
Row orgHeader = orgSheet.createRow(0);
orgHeader.createCell(0).setCellValue("企业ID");
orgHeader.createCell(1).setCellValue("单位名称");
orgHeader.createCell(2).setCellValue("单位Code");
int orgRow = 1;
int startRow = 2;
String orgDataNameNamePrefix = "_";
// 写入所有单位数据
for (Map.Entry<String, List<Org>> entry : orgMap.entrySet()) {
String companyId = entry.getKey();
List<Org> orgs = entry.getValue();
if (CollUtil.isNotEmpty(orgs)) {
for (Org org : orgs) {
Row row = orgSheet.createRow(orgRow++);
row.createCell(0).setCellValue(companyId);
row.createCell(1).setCellValue(org.getShortName());
row.createCell(2).setCellValue(org.getCode()); // 正确写入code
}
String referenceStr = StrUtil.format("OrgData!$B${}:$B${}", startRow, orgRow);
log.info("{} ::: {}", orgDataNameNamePrefix + companyId, referenceStr);
createName(workbook, orgDataNameNamePrefix + companyId, referenceStr);
startRow = orgRow + 1;
}
}
log.info("最终的startRow: {}", startRow);
createName(workbook, "OrgDataMapping", "OrgData!$B$2:$C$" + (startRow - 1));
// 保护工作表
companySheet.protectSheet("protected");
orgSheet.protectSheet("protected");
}
// 创建名称管理器还有引用范围
private void createName(Workbook workbook, String nameName, String reference) {
if (workbook instanceof XSSFWorkbook) {
XSSFName name = ((XSSFWorkbook) workbook).createName();
name.setNameName(nameName);
name.setRefersToFormula(reference);
}
}
3. 设置数据有效性,和根据这一列自动填充值的方法
我的E列的数据是根据Y列的数据设置的数据有效性,所以我把Y列用了 _+id的方式做了 名称管理器 引用。所以给E列设置 formula的时候直接写 下百年的代码就可以了, 通过
INDIRECT函数引用对应的名称管理器
javaStrUtil.format("INDIRECT("_"&X{})", rowIndex + 1)
java
private void setCompanyValidationAndFormula(Sheet sheet, Workbook workbook) {
XSSFDataValidationHelper dvHelper = new XSSFDataValidationHelper((XSSFSheet) sheet);
CellRangeAddressList companyRange;
String formula;
String orgIdFormula;
XSSFDataValidationConstraint companyConstraint;
XSSFDataValidation companyValidation;
Row row;
// 2. 创建锁定样式用于受检企业ID列
CellStyle lockedStyle = workbook.createCellStyle();
lockedStyle.setLocked(true);
for (int rowIndex = 1; rowIndex <= templateEndRow; rowIndex++) {
// 只为当前行的第4列(索引3)设置数据验证
companyRange = new CellRangeAddressList(rowIndex, rowIndex, 3, 3);
// 判断A列选中的数据中是否有删选条件
// 有的话用 trueName这个名称管理器 否则用falseName 这个要根据自己的条件还有业务逻辑自己修改一下
formula = StrUtil.format("=IF(ISNUMBER(FIND("筛选条件",A{})),trueName,falseName)", rowIndex + 1);
log.info("当前函数公式 {}", formula);
companyConstraint = (XSSFDataValidationConstraint)
dvHelper.createFormulaListConstraint(formula);
companyValidation = (XSSFDataValidation)
dvHelper.createValidation(companyConstraint, companyRange);
companyValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
companyValidation.createErrorBox("输入错误", "请从下拉列表选择受检企业");
companyValidation.setSuppressDropDownArrow(true); // 对于动态公式,建议隐藏下拉箭头
sheet.addValidationData(companyValidation);
row = sheet.getRow(rowIndex) != null ? sheet.getRow(rowIndex) : sheet.createRow(rowIndex);
Cell idCell = row.createCell(23);
// 设置VLOOKUP公式,根据D列查找对应的ID CompanyIdMap可以从 公式 名称管理器中查询到
orgIdFormula = StrUtil.format("IFERROR(VLOOKUP($D{}, CompanyIdMap, 2, FALSE), "")", rowIndex + 1);
idCell.setCellFormula(orgIdFormula);
// 应用锁定样式
idCell.setCellStyle(lockedStyle);
}
}
4. 设置指定列只能填写日期类型和字典项数据有效性
java
private void restrictCell2DateFormat(Sheet sheet, int col) {
CellRangeAddressList regions = new CellRangeAddressList(1, templateEndRow, col, col);
XSSFDataValidationHelper dvHelper = new XSSFDataValidationHelper((XSSFSheet) sheet);
// 设置数据验证约束:日期格式,介于最小日期和最大日期之间
XSSFDataValidationConstraint dvConstraint = (XSSFDataValidationConstraint) dvHelper.createDateConstraint(
XSSFDataValidationConstraint.OperatorType.BETWEEN,
"DATE(1900,1,1)", // 最小日期
"DATE(2100,12,31)", // 最大日期
"yyyy-mm-dd" // 日期格式
);
// 创建数据验证规则
XSSFDataValidation validation = (XSSFDataValidation) dvHelper.createValidation(dvConstraint, regions);
// 设置验证失败时的提示信息
validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
validation.createErrorBox("输入错误", "请输入正确的日期格式:yyyy-MM-dd");
// 设置输入提示信息
validation.createPromptBox("日期格式提示", "请输入 yyyy-MM-dd 格式的日期,例如:2024-01-01");
validation.setShowPromptBox(true);
validation.setSuppressDropDownArrow(false);
// 将数据验证添加到工作表
sheet.addValidationData(validation);
// 设置单元格格式为日期格式
CellStyle dateCellStyle = sheet.getWorkbook().createCellStyle();
DataFormat format = sheet.getWorkbook().createDataFormat();
dateCellStyle.setDataFormat(format.getFormat("yyyy-MM-dd"));
sheet.addValidationData(validation);
}
java
/**
* 设置字典项数据有效性
*
* @param dictType
*/
private void setDictDataValidation(Sheet sheet, String dictType, int col) {
Map<String, String> dictMap = dictUtil.getDictMap(dictType);
log.info("当前获取的dictMap {}", dictMap);
DataValidationHelper dvHelper = sheet.getDataValidationHelper();
DataValidationConstraint constraint = dvHelper.createExplicitListConstraint(dictMap.values().toArray(new String[0]));
CellRangeAddressList cellRangeAddressList = new CellRangeAddressList(1, templateEndRow, col, col);
DataValidation validation = dvHelper.createValidation(constraint, cellRangeAddressList);
// 显示下拉箭头
validation.setSuppressDropDownArrow(true);
// 显示提示框
validation.setShowPromptBox(true);
// 设置为INFO级别允许输入
validation.setErrorStyle(DataValidation.ErrorStyle.INFO);
sheet.addValidationData(validation);
}