jxls2.10实现模板导出/单元格合并/自定义标签实现单元格隐藏
目录
- 前言
- 需求
- 项目环境
- 记住!一定要先把模板里的单元格合并先去掉,防止标签定位有问题!
- 代码
- Excel上的标签准备工作
- jxls标签语句
- 区域定位
- 循环渲染
- 单元格合并(重点)
前言
最近接手一个超紧急项目,需要做Excel导出,这个excel排版非常逆天,以至于我从看到排版的第一眼就认为一定要预先准备一个模板,然后将数据写入进去。另外要说一下,数据也是我通过接口向上游系统请求的。其实啊,本来我也没把这个导出当个事,因为我之前就做过,在固定位置写标签然后写入,工具类都是现成的,不过那个项目用的是jxls1.0.6,那个版本作者已经弃更了,而且会跟poi5.X产生兼容问题,所以我果断选择jxls2.X.
jxls官网地址:https://jxls.sourceforge.net/jxls-2.x/reference/custom_command.html
资料很少,我估计你看了也白看。
需求
看到上面那逆天的排版,你就知道,这里面有对象有列表,最需要考虑的是当渲染List数据的时候,左边的单元格合并需要重新合并。第二,我的模板里有些预设的字段可能会是空,客户要求如果是空那么excel上就不展示。这就要求这个模板的有些字段要随着条件的显示。
项目环境
我的项目是基于ruoyi-flex实现的,Spring boot3.2.5,当然这倒是无所谓,poi版本5.2.5(重要,因为3和5会有兼容问题),jxls版本2.10.0
<dependency>
<groupId>org.jxls</groupId>
<artifactId>jxls</artifactId>
<version>2.10.0</version>
</dependency>
<dependency>
<groupId>org.jxls</groupId>
<artifactId>jxls-poi</artifactId>
<version>2.10.0</version>
</dependency>
上面的excel模板过于逆天,我就不一比一还原了,我就简单做了个模板。而且模板在公司里,也传不出来。
记住!一定要先把模板里的单元格合并先去掉,防止标签定位有问题!
代码
package com.ruoyi.web.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.core.util.StrUtil;
import com.aizuda.easy.retry.common.core.util.JsonUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.util.MapUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.ruoyi.common.core.core.domain.AjaxResult;
import com.ruoyi.common.excel.utils.ExcelUtil;
import com.ruoyi.common.json.utils.JsonUtils;
import com.ruoyi.web.util.HiddenCommand;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.jxls.builder.xls.XlsCommentAreaBuilder;
import org.jxls.common.Context;
import com.ruoyi.common.web.core.BaseController;
import com.ruoyi.web.domain.model.DemoGoods1;
import com.ruoyi.web.service.DemoGoodsService;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jxls.util.JxlsHelper;
import org.springframework.web.bind.annotation.*;
import java.io.*;
import java.net.URLEncoder;
import java.util.*;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/goods")
@Slf4j
@SaIgnore
public class DemoGoodsController extends BaseController {
public static void main(String[] args) throws IOException {
Map<String, Object> map = new HashMap<>();
String userStr = "{\n" +
" \"name\": \"李雨桐\",\n" +
" \"idcard\": \"1\",\n" +
" \"userid\": \"1\",\n" +
" \"gw\": \"1\",\n" +
" \"sex\": \"1\",\n" +
" \"birth\": \"1\",\n" +
" \"bm\": \"1\",\n" +
" \"bm\": \"1\",\n" +
" \"zt\": \"1\"\n" +
"}";
JSONObject userObj = JSON.parseObject(userStr);
map.put("user", userObj);
String gzzStr = "{\n" +
" \"ndkhqk\": \"you\",\n" +
" \"lcjlghsl\": \"1\",\n" +
" \"hxkhcfzs\": \"2\",\n" +
" \"khjlghsl\": \"3\",\n" +
" \"yjxhwtsl\": \"4\",\n" +
" \"ywccl\": \"5\",\n" +
" \"list\": [\n" +
" {\n" +
" \"khmc\": \"test1\",\n" +
" \"sxye\": \"20\",\n" +
" \"sxzt\": \"1\"\n" +
" },\n" +
" {\n" +
" \"khmc\": \"test2\",\n" +
" \"sxye\": \"20\",\n" +
" \"sxzt\": \"1\"\n" +
" },\n" +
" {\n" +
" \"khmc\": \"test3\",\n" +
" \"sxye\": \"20\",\n" +
" \"sxzt\": \"1\"\n" +
" },\n" +
" {\n" +
" \"khmc\": \"test4\",\n" +
" \"sxye\": \"20\",\n" +
" \"sxzt\": \"1\"\n" +
" }\n" +
" ]\n" +
"}";
JSONObject gzzObj = JSON.parseObject(gzzStr);
map.put("gzzl", gzzObj);
InputStream is = null;
OutputStream os = null;
try {
// 读取模板
is = DemoGoodsController.class.getClassLoader().getResourceAsStream("测试.xlsx");
// 新建文件
File file = new File("E:/export.xlsx");
// 输出流
os = new BufferedOutputStream(new FileOutputStream(file));
//构建数据
Context context = new Context(map);
// Context context = buildData();
XlsCommentAreaBuilder.addCommandMapping(HiddenCommand.COMMAND_NAME, HiddenCommand.class);
// 数据写入模板
JxlsHelper.getInstance().processTemplate(is, os, context);
// }
} catch (Exception e) {
e.printStackTrace();
} finally {
is.close();
os.close();
}
}
}
模板就放在springboot静态资源路径里就可以了
自定义标签类实现
package com.ruoyi.web.util;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.jxls.area.Area;
import org.jxls.command.AbstractCommand;
import org.jxls.command.Command;
import org.jxls.common.CellRef;
import org.jxls.common.Context;
import org.jxls.common.Size;
import org.jxls.transform.poi.PoiTransformer;
import org.jxls.util.Util;
public class HiddenCommand extends AbstractCommand {
public static final String COMMAND_NAME = "hiddenDef";
public static final String ROW = "row";
public static final String COL = "col";
/**隐藏行还是列 行:row, 列:col*/
private String hidTag;
/**条件*/
private String condition;
private Area area;
@Override
public String getName() {
return COMMAND_NAME;
}
@Override
public Command addArea(Area area) {
if (!super.getAreaList().isEmpty()) {
throw new IllegalArgumentException("You can add only a single area to 'merge' command");
}
this.area = area;
return super.addArea(area);
}
@Override
public Size applyAt(CellRef cellRef, Context context) {
Boolean conditionResult = Util.isConditionTrue(getTransformationConfig().getExpressionEvaluator(), condition, context);
if (conditionResult) {
PoiTransformer transformer = (PoiTransformer)this.getTransformer();
Sheet sheet = transformer.getWorkbook().getSheet(cellRef.getSheetName());
CellRef srcCell = area.getStartCellRef();
if(ROW.equals(hidTag)) {
int rowNub = srcCell.getRow();
Row row = sheet.getRow(rowNub);
// short rowHeight = 0;
row.setZeroHeight(true);
// row.setHeight(rowHeight);
}
if(COL.equals(hidTag)) {
int col = srcCell.getCol();
sheet.setColumnHidden(col,true);
}
}
return area.applyAt(cellRef, context);
}
public String getHidTag() {
return hidTag;
}
public void setHidTag(String hidTag) {
this.hidTag = hidTag;
}
public String getCondition() {
return condition;
}
public void setCondition(String condition) {
this.condition = condition;
}
}
Excel上的标签准备工作
再给你们熟悉一下,我的模拟Excel模板
我们来看下数据渲染成功后的样子
我们要考虑:
- 单元格的合并
- C6那里,基本信息-服务年限如果没有数据要隐藏起来
- C10那里,不良授信业务情况是个List数据
jxls标签语句
快捷键shift+F2,如要删除请右击点击删除注释,这个标签语句都是写在注释里的。
区域定位
jx:area(lastCell="F12")
这个注释写在标题头那里即可,我看大家都是这么写的,官网的例子也是的。
对应的官网文档地址:https://jxls.sourceforge.net/jxls-2.x/reference/xls_area.html
我的理解就是,jxls需要读取表格的作用域范围,你要给他最后的行在哪里,我这里是F12。
请注意:我最后一行有单元格合并,所以我是先把单元格合并给去了,获取到准确的定位,然后再重新定位的,lastCell参数的准确性是一定要保证的!
循环渲染
jx:each(items="gzzl.list" var="ywqk" lastCell="F11")
标签注释写在循环标签的第一个就可以了,关键是lastCell的位置要写对。
each是循环关键字
items:就是你传入的map的属性
var:变量别名
lastCell作用域的尾部定位(重要)
单元格合并(重点)
jx:mergeCells(rows="gzzl.list.size()+5" lastCell="A12")
标签注释写在对应的title那即可,关键是lastCell的位置要写对。
mergeCells:合并单元格关键字
rows:合并多少行
lastCell作用域的尾部定位(重要)
我这里合并的逻辑很简单,你对照excel就会明白,因为你的单元格会随着list数据渲染而增多,所以在计算行数的时候是把list的容量和多出来的空行相加(下图选中的部分就是多出来的空行)。
这里我要强调mergeCell的lastCell,一开始我并不懂lastCell参数在这里怎么写,我就写当前标签的位置,结果就导致渲染数据之后,单元格整体错乱。所以这里lastCell一定要写你要合并到模板的哪里?像我这里就是合并到差错情况那一行,就是A12。同理,管护情况和不良授信业务情况也是同理,管护情况的rows就是gzzl.list.size()+3,不良授信业务情况就是gzzl.list.size()+1
基本信息那里是合并两列,lastCell就写B6,自动两列合并。
写到这里,如果你的模板是纯静态的,那么你基本上就解决了这个问题,但是我这边改了需求,要求基本信息里的服务年限,如果没有传值那就不展示这个标签。这怎么办?jxls有对应的标签处理吗?很遗憾,没有,jxls里提供的标签太少了,需要自定义标签。我这里的逻辑是把不展示的标签给隐藏起来,我担心删除掉会影响标签读取继而导致表格错乱之类的问题。代码上面已经给了,我们来看excel里面怎么配。
hidTag是定义的隐藏行还是列,我这里隐藏行。
代码我是参照这篇帖子的:https://blog.csdn.net/fascinatingGirl/article/details/107541969
因为官网自定义标签的文档只有寥寥数字,你就对着官网可劲学吧!
最后展示的效果:
能看到,第六行已经被隐藏起来了,这算是一种折中的处理方式,本来嘛如果我没有解决方案的话,我要么就打算展示空标签,反正也不是什么重要的事情跟客户说一下问题也不大。要么就让前端去做,这是若依群给我的方案,说前端可以把渲染的表格导出成excel,我看了几篇帖子好像是可以,但是他们的例子都比较简单,没有我这种左边的列需要合并的。