python网站开发基础个人博客首页
目录
- 前言
- 需求
- 项目环境
- 记住!一定要先把模板里的单元格合并先去掉,防止标签定位有问题!
- 代码
- 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;@Overridepublic String getName() {return COMMAND_NAME;}@Overridepublic 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);}@Overridepublic 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,我看了几篇帖子好像是可以,但是他们的例子都比较简单,没有我这种左边的列需要合并的。