找回密码
立即注册
搜索
发回帖 发新帖

6240

积分

0

好友

761

主题
发表于 1 小时前 | 查看: 2| 回复: 0

最近接到一个需求:快速生成一份包含文字、图片和数据报表的 Word 报告,而且图表数据要能灵活调整。一开始想到的是 Apache POI,但翻了不少资料后,发现 poi-tl 这个基于 Apache POI 的模板引擎更适合手头的业务场景——上手简单、代码量也少。于是花时间研究了一轮,顺手把常用的标签封装成了一个工具类,方便后续快速复用。

话不多说,先看 poi-tl 到底是什么。

1、POI-TL 简介

poi-tl 是一个免费开源的 Java 类库,基于 Apache POI 实现,本质上是一个纯 Java 组件模板引擎。它跨平台、代码短小精悍,通过插件机制具备高度扩展性。

使用方式和 Freemarker 类似,需要预先设置好模板,但整体更简单、也更容易上手。

2、常用标签

2.1 文本

格式:{{var}}

数据模型:

类型 描述
String 纯文本
TextRenderData 带有样式的文本
HyperLinkTextRenderData 超链接文本
Object 调用 toString() 方法转化成文本

例如:

put("name", "Sayi");
put("author", new TextRenderData("000000", "Sayi"));
// 超链接
put("link", new HyperLinkTextRenderData("website", "http://deepoove.com"));

2.2 图片

格式:以 @ 开始,{{@var}}

数据模型:

类型 描述
PictureRenderData 支持本地图片,也支持远程 URL 地址(将 URL 图片转成 BufferedImage)

例如:

// 本地图片
put("local", new PictureRenderData(80, 100, "./sayi.png"));

// 图片流
put("localbyte", new PictureRenderData(80, 100, ".png", new FileInputStream("./logo.png")));

// 网络图片(注意网络耗时对系统可能的性能影响)
put("urlpicture", new PictureRenderData(50, 50, ".png",
        BytePictureUtils.getUrlBufferedImage("http://deepoove.com/images/icecream.png")));

2.3 表格

poi-tl 默认实现了 N 行 N 列的表格样式,同时提供了数据为空时展示一行空数据的文案。

格式:以 # 开头,{{#var}}

数据模型:

类型 描述
MiniTableRenderData 主要调用 TextRenderData,提供样式等

2.4 列表

格式:以 * 开头,{{*var}}

数据模型:

类型 描述
NumbericRenderData 主要调用 TextRenderData,提供样式等

NumbericRenderData 支持多种列表样式,包括罗马字符、有序无序等:

FMT_DECIMAL            // 1. 2. 3.
FMT_DECIMAL_PARENTHESES // 1) 2) 3)
FMT_BULLET             // ● ● ●
FMT_LOWER_LETTER       // a. b. c.
FMT_LOWER_ROMAN        // i ⅱ ⅲ
FMT_UPPER_LETTER       // A. B. C.

2.5 单系列图表

单系列图表,指的是图形中只展示一列数据,例如单数据的柱状图、饼图等。

格式:先创建单系列图,然后在图表区格式 -> 可选文字 -> 标题,与文字一样,以 {{val}} 命名,如图:

POI-TL 单系列饼图标题设置示例

数据模型:

类型 描述
ChartSingleSeriesRenderData 提供了设置标题、类别等方法

例如:

ChartSingleSeriesRenderData singleSeriesRenderData = new ChartSingleSeriesRenderData();
singleSeriesRenderData.setCategories(new String[] { "俄罗斯", "加拿大", "美国", "中国" });
singleSeriesRenderData.setChartTitle("测试");
pie.setSeriesData(new SeriesRenderData("countries", new Integer[] { 17098242, 9984670, 9826675, 9596961 }));

2.6 多系列图表

在报表应用中,很多时候使用的是多系列组合,例如柱状图与折线图组合等。

格式与单系列一致:

POI-TL 多系列组合图表标签设置示例

数据模型:

类型 描述
ChartMultiSeriesRenderData 提供了设置标题、类别等方法

例如:

ChartMultiSeriesRenderData chart = new ChartMultiSeriesRenderData();
chart.setChartTitle("MyChart");
chart.setCategories(new String[] { "中文", "English" });
List<SeriesRenderData> seriesRenderData = new ArrayList<>();
seriesRenderData.add(new SeriesRenderData("countries", new Double[] { 15.0, 6.0 }));
seriesRenderData.add(new SeriesRenderData("speakers", new Double[] { 223.0, 119.0 }));
chart.setSeriesDatas(seriesRenderData);

3、代码封装

以上介绍了几种常用标签,更多标签可以参考官方文档。既然已经清楚了标签的用法,接下来做一轮代码整合——封装一个工具类,快速实现多标签一起生成报表。

引入 jar 包,以 1.8.2 版本为例:

<dependency>
    <groupId>com.deepoove</groupId>
    <artifactId>poi-tl</artifactId>
    <version>1.8.2</version>
</dependency>

3.1 创建标签类型

标签类型用于声明程序支持哪些标签,例如文字、图片等。使用枚举实现,方便后期扩展。

/**
 * @author: jiangjs
 * @description: 标签类型
 **/
public enum WordContentTypeEnum {
    /**
     * 文本
     */
    TEXT,
    /**
     * 图片
     */
    PICTURE,
    /**
     * 表格
     */
    TABLE,
    /**
     * 列表
     */
    LIST,
    /**
     * 图表
     */
    CHART;
}

3.2 创建公共实体

该实体用于描述替换的标签名称及标签类型。标签名称即 put 使用的名称,与 Word 模板中定义的名称一致。

/**
 * @author: jiangjs
 * @description: 公共实体
 * @date: 2022/11/24 15:05
 **/
@Data
@Accessors(chain = true)
public class LabelData {
    /**
     * 标签名称,即 put 使用到的名称
     */
    private String labelName;
    /**
     * 文件内容类型
     */
    private WordContentTypeEnum typeEnum;
}

3.3 封装统一数据生成接口

该接口只提供各标签数据生成的封装,返回一个 Object。

/**
 * @author: jiangjs
 * @description: 封装统一各标签数据生成接口
 **/
public interface GenerateWord {
    Object generateWord(LabelData data);
}

3.4 创建工厂

主要用于对各标签生成的数据进行统一管理。

/**
 * @author: jiangjs
 * @description: 生成word工厂
 **/
public class GenerateWordFactory {

    private static final Map<WordContentTypeEnum, GenerateWord> TYPE_BACK_DATA = new HashMap<>();

    public static void register(WordContentTypeEnum typeEnum, GenerateWord word) {
        if (Objects.nonNull(typeEnum)) {
            TYPE_BACK_DATA.put(typeEnum, word);
        }
    }

    public static GenerateWord getBackData(WordContentTypeEnum typeEnum) {
        return TYPE_BACK_DATA.get(typeEnum);
    }
}
  • TYPE_BACK_DATA:各标签封装数据的类与标签类型的一一对应关系。
  • GenerateWord getBackData(WordContentTypeEnum typeEnum):根据标签类型获取对应的数据生成方法。

3.5 封装 Word 生成类

这个类的主要作用是将封装的各标签数据写入 Word 模板,并生成最终报表。

/**
 * @author: jiangjs
 * @description: 操作word内容
 **/
public class OperateWordManage {

    private final static Logger log = LoggerFactory.getLogger(OperateWordManage.class);

    public static void generateWordContent(File tempFileFile, String destFilePath, List<LabelData> contents) {
        FileOutputStream fos = null;
        XWPFTemplate template = null;
        try {
            template = XWPFTemplate.compile(tempFileFile).render(new HashMap<String, Object>(contents.size()) {{
                contents.forEach(content -> {
                    GenerateWord backData = GenerateWordFactory.getBackData(content.getTypeEnum());
                    put(content.getLabelName(), backData.generateWord(content));
                });
            }});
            fos = new FileOutputStream(destFilePath);
            template.write(fos);
            fos.flush();
        } catch (Exception e) {
            log.error("替换生成图表报错:{}", e.getMessage());
            e.printStackTrace();
        } finally {
            try {
                if (Objects.nonNull(fos)) {
                    fos.close();
                }
                if (Objects.nonNull(template)) {
                    template.close();
                }
            } catch (Exception e) {
                log.error("关闭数据流报错:{}", e.getMessage());
                e.printStackTrace();
            }
        }
    }
}
  • tempFileFile:模板文件地址。
  • destFilePath:生成后的文件地址。
  • List<LabelData> contents:各标签封装后的数据集合。

3.6 各标签生成数据封装

3.6.1 文本封装

3.6.1.1 创建文本实体

文本实体涵盖纯文本、带样式文本、超链接文本三类。

/**
 * @author: jiangjs
 * @description: 文本
 **/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class TextContentData extends LabelData {

    /**
     * 纯文本内容
     */
    private String content;
    /**
     * 带样式文本
     */
    private TextRenderData renderData;
    /**
     * 超链接文本
     */
    private HyperLinkTextRenderData linkData;
}
3.6.1.2 创建数据封装类

该封装类根据不同的属性值返回不同类型,优先返回超链接,其次是带样式的文本,最后是纯文本。

/**
 * @author: jiangjs
 * @description: 文本内容实现
 **/
@Component
public class TextGenerateWord implements GenerateWord {

    @PostConstruct
    public void init() {
        GenerateWordFactory.register(WordContentTypeEnum.TEXT, this);
    }

    @Override
    public Object generateWord(LabelData data) {
        TextContentData contentData = (TextContentData) data;
        return Objects.nonNull(contentData.getLinkData()) ? contentData.getLinkData() :
                Objects.nonNull(contentData.getRenderData()) ? contentData.getRenderData() :
                        contentData.getContent();
    }
}

每个数据封装方法都有一个初始化方法,主要作用是调用工厂注册方法,将标签类型与当前数据生成方法绑定。

3.6.2 图片封装

3.6.2.1 创建图片实体
/**
 * @author: jiangjs
 * @description: 图片
 **/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class PictureContentData extends LabelData {

    /**
     * 图片宽度
     */
    private Integer width;
    /**
     * 图片高度
     */
    private Integer height;
    /**
     * 图片类型
     */
    private PicTypeEnum picType;
    /**
     * 图片地址(网络图片插入时使用)
     */
    private String picUrl;
    /**
     * 图片文件
     */
    private File file;
}

PicTypeEnum:图片类型。

/**
 * @author: jiangjs
 * @description: 图片类型
 **/
public enum PicTypeEnum {
    /**
     * png图片
     */
    PNG(".png"),
    /**
     * JPG图片
     */
    JPG(".jpg"),
    /**
     * jpeg
     */
    JPEG(".jpeg");

    private final String picName;

    PicTypeEnum(String picName) {
        this.picName = picName;
    }

    public String getPicName() {
        return picName;
    }
}
3.6.2.2 创建数据封装类
/**
 * @author: jiangjs
 * @description: 图片内容实现
 **/
@Component
public class PictureGenerateWord implements GenerateWord {

    @PostConstruct
    private void init() {
        GenerateWordFactory.register(WordContentTypeEnum.PICTURE, this);
    }

    @Override
    public Object generateWord(LabelData data) {
        PictureContentData picture = (PictureContentData) data;
        return StringUtils.isNotBlank(picture.getPicUrl()) ?
                new PictureRenderData(picture.getWidth(), picture.getHeight(), picture.getPicType().getPicName(),
                        BytePictureUtils.getUrlBufferedImage(picture.getPicUrl()))
                : new PictureRenderData(picture.getWidth(), picture.getHeight(), picture.getFile());
    }
}

该封装会判断图片实体中是否包含图片链接,从而创建对应的数据对象。

3.6.3 表格封装

3.6.3.1 创建表格实体
/**
 * @author: jiangjs
 * @description: 表格
 **/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class TableSeriesRenderData extends LabelData {

    /**
     * 表头
     */
    private TextRenderData[] header;
    /**
     * 表内容
     */
    private List<TextRenderData[]> contents;
}

将表头与表格内容分开赋值,使结构更清晰。

3.6.3.2 创建数据封装类
/**
 * @author: jiangjs
 * @description: 表格内容实现
 **/
@Component
public class TableGenerateWord implements GenerateWord {

    @PostConstruct
    private void init() {
        GenerateWordFactory.register(WordContentTypeEnum.TABLE, this);
    }

    @Override
    public Object generateWord(LabelData data) {
        TableSeriesRenderData tableData = (TableSeriesRenderData) data;
        RowRenderData header = RowRenderData.build(tableData.getHeader());
        List<RowRenderData> contentData = new ArrayList<>();
        tableData.getContents().forEach(con -> {
            contentData.add(RowRenderData.build(con));
        });
        return new MiniTableRenderData(header, contentData);
    }
}

3.6.4 列表封装

3.6.4.1 创建列表实体
/**
 * @author: jiangjs
 * @description: 列表
 **/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class ListRenderData extends LabelData {

    /**
     * 列表数据集
     */
    private List<TextRenderData> list;

    /**
     * 列表样式,支持罗马字符、有序无序等,默认为点
     */
    private Pair<STNumberFormat.Enum, String> pair = NumbericRenderData.FMT_BULLET;
}
3.6.4.2 创建数据封装类
/**
 * @author: jiangjs
 * @description: 列表内容实现
 **/
@Component
public class ListGenerateWord implements GenerateWord {

    @PostConstruct
    private void init() {
        GenerateWordFactory.register(WordContentTypeEnum.LIST, this);
    }

    @Override
    public Object generateWord(LabelData data) {
        ListRenderData listData = (ListRenderData) data;
        return new NumbericRenderData(listData.getPair(), listData.getList());
    }
}

3.6.5 图表封装

3.6.5.1 创建图表实体
/**
 * @author: jiangjs
 * @description: 图表
 **/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class ChartSeriesRenderData extends LabelData {

    /**
     * 横轴数据
     */
    private String[] categories;

    /**
     * 图表名称
     */
    private String title;

    /**
     * 图表类型 组合
     */
    private CharCombinationType charType = CharCombinationType.MULTI;

    /**
     * 系列对应数据
     */
    private List<RenderData> senderData;

    @Data
    public static class RenderData {
        /**
         * 系列名称
         */
        private String renderTitle;
        /**
         * 系列对应的数据
         */
        private Number[] data;
        /**
         * 该系列对应生成的图表类型
         */
        private SeriesRenderData.ComboType comboType = null;
    }
}

CharCombinationType 表示图表中系列的类型,只有单系列或多系列两种。

/**
 * @author: jiangjs
 * @description: 图表系列类型
 **/
public enum CharCombinationType {

    /**
     * 多组合
     */
    MULTI("Multi"),
    /**
     * 单图形
     */
    Single("Single");

    private final String type;

    CharCombinationType(String type) {
        this.type = type;
    }

    public String getType() {
        return type;
    }
}
3.6.5.2 创建数据封装类
/**
 * @author: jiangjs
 * @description: 图表类型
 **/
@Component
public class ChartGenerateWord implements GenerateWord {

    @PostConstruct
    private void init() {
        GenerateWordFactory.register(WordContentTypeEnum.CHART, this);
    }

    @Override
    public Object generateWord(LabelData obj) {
        ChartSeriesRenderData renderData = (ChartSeriesRenderData) obj;
        if (Objects.nonNull(renderData.getCharType()) && Objects.equals("Single", renderData.getCharType().getType())) {
            ChartSingleSeriesRenderData singleSeriesRenderData = new ChartSingleSeriesRenderData();
            singleSeriesRenderData.setCategories(renderData.getCategories());
            singleSeriesRenderData.setChartTitle(renderData.getTitle());
            ChartSeriesRenderData.RenderData seriesData = renderData.getSenderData().get(0);
            SeriesRenderData srd = new SeriesRenderData(seriesData.getRenderTitle(), seriesData.getData());
            if (Objects.nonNull(seriesData.getComboType())) {
                srd.setComboType(seriesData.getComboType());
            }
            singleSeriesRenderData.setSeriesData(srd);
            return singleSeriesRenderData;
        } else {
            ChartMultiSeriesRenderData seriesRenderData = new ChartMultiSeriesRenderData();
            seriesRenderData.setCategories(renderData.getCategories());
            seriesRenderData.setChartTitle(renderData.getTitle());
            List<ChartSeriesRenderData.RenderData> renderDataList = renderData.getSenderData();
            List<SeriesRenderData> groupData = new ArrayList<>();
            renderDataList.forEach(data -> {
                SeriesRenderData srd = new SeriesRenderData(data.getRenderTitle(), data.getData());
                if (Objects.nonNull(data.getComboType())) {
                    srd.setComboType(data.getComboType());
                }
                groupData.add(srd);
            });
            seriesRenderData.setSeriesDatas(groupData);
            return seriesRenderData;
        }
    }
}

代码会根据系列类型的不同,创建不同的数据类型返回。

至此,常用标签的数据封装已经完成,进入测试环节。

4、测试

4.1 创建文件模板

根据前面介绍的常用标签,在 Word 中创建文件模板,如图:

POI-TL Word 模板代码与柱状图预览

将模板放在项目的 resources 目录下:

项目资源目录存放 Word 模板文件

4.2 封装各标签数据

根据封装后的标签数据生成类来进行数据组织。

private static final String TEMPLATE_PATH = "static/template/demo_template.docx";

public void generateCharts() {
    File templateFile = null;
    try {
        templateFile = new ClassPathResource(TEMPLATE_PATH).getFile();
    } catch (IOException e) {
        e.printStackTrace();
    }
    List<LabelData> generates = new ArrayList<>();

    // 文本
    TextContentData contentData = new TextContentData();
    contentData.setContent("2022年月通报函生成报告").setLabelName("title").setTypeEnum(WordContentTypeEnum.TEXT);
    generates.add(contentData);

    // 带样式文本
    TextContentData typeData = new TextContentData();
    typeData.setRenderData(new TextRenderData("cc0000", "这是带样式的内容"))
            .setLabelName("typeContent").setTypeEnum(WordContentTypeEnum.TEXT);
    generates.add(typeData);

    // 插入图片
    PictureContentData picData = new PictureContentData();
    picData.setWidth(200).setHeight(160).setPicType(PicTypeEnum.JPG)
            .setFile(new File("D:\\down\\java.jpg"))
            .setLabelName("picture").setTypeEnum(WordContentTypeEnum.PICTURE);
    generates.add(picData);

    // 插入表格
    TableSeriesRenderData tableData = new TableSeriesRenderData();
    List<TextRenderData[]> contents = Arrays.asList(
            new TextRenderData[]{new TextRenderData("科教1班"), new TextRenderData("1")},
            new TextRenderData[]{new TextRenderData("幼儿3班"), new TextRenderData("6")});
    tableData.setHeader(new TextRenderData[]{new TextRenderData("班级"), new TextRenderData("排名")})
            .setContents(contents).setLabelName("showTable").setTypeEnum(WordContentTypeEnum.TABLE);
    generates.add(tableData);

    // 插入列表
    ListRenderData listRenderData = new ListRenderData();
    List<TextRenderData> listData = Arrays.asList(
            new TextRenderData("排序1"),
            new TextRenderData("排序2"),
            new TextRenderData("排序3"));
    listRenderData.setList(listData).setPair(NumbericRenderData.FMT_LOWER_ROMAN)
            .setTypeEnum(WordContentTypeEnum.LIST).setLabelName("numList");
    generates.add(listRenderData);

    // 折线
    ChartSeriesRenderData lineData = new ChartSeriesRenderData();
    List<ChartSeriesRenderData.RenderData> lineRenderData = new ArrayList<>();
    ChartSeriesRenderData.RenderData numRenderData = new ChartSeriesRenderData.RenderData();
    ChartSeriesRenderData.RenderData moneyRenderData = new ChartSeriesRenderData.RenderData();
    numRenderData.setRenderTitle("项目数量").setData(new Double[] {
            -11.02, -19.42, -10.61, -11.41, -7.91, -5.44, -5.30, -2.75, -1.24, 0.35});
    moneyRenderData.setRenderTitle("投资额").setData(new Number[] {
            -12.66, -19.41, -15.16, -19.72, -17.05, -15.92, -15.10, -13.04, -10.65, -9.15});
    lineRenderData.add(numRenderData);
    lineRenderData.add(moneyRenderData);
    lineData.setTitle("1-10月份全国新开工项目数量、投资额增速")
            .setCategories(new String[] {"1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月"})
            .setSenderData(lineRenderData).setTypeEnum(WordContentTypeEnum.CHART).setLabelName("speedLine");
    generates.add(lineData);

    // 柱状图
    ChartSeriesRenderData barData = new ChartSeriesRenderData();
    List<ChartSeriesRenderData.RenderData> barRenderData = new ArrayList<>();
    ChartSeriesRenderData.RenderData openRenderData = new ChartSeriesRenderData.RenderData();
    ChartSeriesRenderData.RenderData moneyData = new ChartSeriesRenderData.RenderData();
    openRenderData.setRenderTitle("开工数量").setData(new Number[] {
            40, 50, 45, 12, 21, 18, 21, 28, 21, 18, 28, 18, 20, 19,
            -10, -9, -10, 19, 39, 31, 20, 19, -10, -9, -10, 19, 39, 31,
            -10, 19, 39});
    moneyData.setRenderTitle("投资额").setData(new Number[] {
            20, -22, -12, 8, -10, -14, -10, -10, -8, -2, -8, -1, -9, -21,
            -9, -7, -21, -10, 21, -29, -50, -21, -9, -7, -21, -10, 21, -29,
            -21, -10, 21});
    barRenderData.add(openRenderData);
    barRenderData.add(moneyData);
    barData.setTitle("各省(自治区)直辖市新开项目数量、投资额同比情况")
            .setCategories(new String[] {
                    "贵州", "西藏", "黑龙江", "浙江", "湖北", "江苏", "四川", "福建", "安徽", "海南",
                    "山西", "广西", "青海", "广东", "甘肃", "云南", "宁夏", "新疆", "湖南", "北京",
                    "河北", "山西", "山东", "内蒙古", "天津", "江西", "吉林", "河南", "重庆", "上海",
                    "辽宁"})
            .setSenderData(barRenderData).setTypeEnum(WordContentTypeEnum.CHART).setLabelName("investmentRatio");
    generates.add(barData);

    // 生成饼图
    ChartSeriesRenderData areaData = new ChartSeriesRenderData();
    List<ChartSeriesRenderData.RenderData> areaRenderDatas = new ArrayList<>();
    ChartSeriesRenderData.RenderData areaRenderData = new ChartSeriesRenderData.RenderData();
    areaRenderData.setData(new Number[]{17098242, 9984670, 9826675, 9596961}).setRenderTitle("投资额")
            .setComboType(SeriesRenderData.ComboType.AREA);
    areaRenderDatas.add(areaRenderData);
    areaData.setTitle("国家投资额").setSenderData(areaRenderDatas).setCharType(CharCombinationType.Single)
            .setCategories(new String[]{"俄罗斯", "加拿大", "美国", "中国"})
            .setLabelName("areaShow").setTypeEnum(WordContentTypeEnum.CHART);
    generates.add(areaData);

    // 横向柱状图
    ChartSeriesRenderData lateralData = new ChartSeriesRenderData();
    List<ChartSeriesRenderData.RenderData> lateralRenderData = new ArrayList<>();
    ChartSeriesRenderData.RenderData lateralYearData = new ChartSeriesRenderData.RenderData();
    ChartSeriesRenderData.RenderData lateralMoneyData = new ChartSeriesRenderData.RenderData();
    lateralYearData.setRenderTitle("2021年").setData(new Number[]{400, 200});
    lateralMoneyData.setRenderTitle("2022年").setData(new Number[]{456, 255});
    lateralRenderData.add(lateralYearData);
    lateralRenderData.add(lateralMoneyData);
    lateralData.setTitle("工程建设项目建设周期同比情况")
            .setCategories(new String[]{"从立项到开工的用时", "从开工到验收的用时"})
            .setSenderData(lateralRenderData).setTypeEnum(WordContentTypeEnum.CHART).setLabelName("cycleRadio");
    generates.add(lateralData);

    // 组合图表
    ChartSeriesRenderData groupData = new ChartSeriesRenderData();
    List<ChartSeriesRenderData.RenderData> groupRenderData = new ArrayList<>();
    ChartSeriesRenderData.RenderData unOpenData = new ChartSeriesRenderData.RenderData();
    ChartSeriesRenderData.RenderData openRadioData = new ChartSeriesRenderData.RenderData();
    unOpenData.setComboType(SeriesRenderData.ComboType.BAR).setRenderTitle("未开工项目数(个)")
            .setData(new Number[]{
                    55, 35, 23, 76, 60, 65.1, 70.2, 75.3, 80.4, 85.5, 90.6, 95.7, 26,
                    76, 60, 65.1, 70.2, 75.3, 80.4, 95.7, 26, 76, 60, 65.1, 70.2, 75.3,
                    95.7, 26, 76, 60, 65.1});
    openRadioData.setComboType(SeriesRenderData.ComboType.LINE).setRenderTitle("开工率(%)")
            .setData(new Number[]{
                    34, 45, 23, 67, 34, 45, 23, 67, 34, 45, 23, 67, 23, 67, 34, 45,
                    23, 45, 23, 67, 23, 67, 34, 45, 23, 45, 67, 23, 67, 34, 45});
    groupRenderData.add(unOpenData);
    groupRenderData.add(openRadioData);
    groupData.setTitle("各省(区、市)签约项目开工情况")
            .setCategories(new String[]{
                    "北京", "吉林", "云南", "上海", "安徽", "浙江", "江西", "四川", "陕西", "甘肃",
                    "江苏", "广西", "内蒙古", "福建", "天津", "海南", "黑龙江", "贵州", "山东", "河北",
                    "辽宁", "湖北", "宁夏", "广东", "重庆", "河南", "新疆", "山西", "湖南", "青海",
                    "兵团"})
            .setSenderData(groupRenderData).setTypeEnum(WordContentTypeEnum.CHART).setLabelName("openCondition");
    generates.add(groupData);

    // 生成 Word
    OperateWordManage.generateWordContent(templateFile, "D:\\down\\output.docx", generates);
}

4.3 生成报表

POI-TL 生成的 Word 报表效果示例

从生成的 Word 中可以看到,模板中的数据已经被替换,图表也在 Word 中成功生成。

上述就是使用 poi-tl 生成 Word 报表的完整过程,并对其做了封装方便复用。工欲善其事,必先利其器,有时封装是必要的,希望这个做法能给你带来一些启发。

源码地址:https://github.com/lovejiashn/generate_report.git




上一篇:小公司用 egg.js 与 uni-app 开发 App 的倒闭复盘及创业避坑建议
下一篇:ThreadSanitizer 漏报根源拆解:影子内存淘汰与无锁内存序盲区
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-11 04:16 , Processed in 0.099513 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表