JasperReports报表模板:Band布局、PDF中文与JSON数据源避坑
简介:《Jasper报表制作小技巧》是一份面向IT开发与报表制作人员的docx实操笔记,聚焦Jaspersoft/Jasper报表从结构认知到常见问题处理的入门与进阶场景。内容围绕Title、Page Header、Column Header、Detail、Column Footer、Page Footer、Summary等报表区域展开,并整理了JSON数据源接入、PDF中文字段设置、手工补录字段、明细分组、序号列变量、自定义纸张大小及交叉报表控件等实用技巧,可帮助读者快速理解band布局并减少实际制表时的反复调试。资源包共1个docx文件,约599KB,篇幅集中、便于按主题查阅。目前已有242人学习或浏览,适合需要借助现成笔记查漏补缺、提升Jasper报表搭建效率的开发者和报表维护人员参考。
1. Jasper 报表制作里最容易翻车的环节,是模板交接
Jasper 报表制作里最容易翻车的环节,是模板交接。前段时间接手一个仓储系统里的入库明细模板,设计器里预览一切正常,编译导出 PDF 后中文全变成方框,翻遍每个控件的字体设置也找不到原因——问题根本不在设计器,而在 PDF 那条独立的渲染链路上。这类工具的门槛从来不在拖控件,而在于搞清楚每个 Band 到底在哪一页出现、字段类型跟数据源对不对得上、导出格式走的是哪条渲染路径。这份技巧集覆盖的正是这些:Title 到 Summary 的九个分区、JSON 数据源接法、PDF 中文字体、分组与序号列、Crosstab 交叉表、自定义纸张面单,以及模板上传后的打印配置与 SQL 条件绑定。适合已经能把报表拖出个样子、但一导出或一上线就出问题的同学。
2. Band 与页面布局:九个分区的触发条件、快捷键与自定义纸张
报表做得好不好,八成取决于设计之前有没有把 Band 的边界想清楚。同一个控件放在 Page Header 和 Column Header 里,最终打印出来的位置天差地别;Summary 放错区域,末页会凭空多出一张空白纸。这一章先把九个分区的语义钉死,再讲设计器里怎么少用鼠标,最后落到面单场景的自定义纸张。
2.1 九个 Band 各在哪一页出现,高度又如何影响分页
先把对照关系摆出来,这是后面所有布局操作的地基。很多人把 Title 当成页眉用,结果第二页标题消失,回头改了半天样式,其实是选错了 Band。
| Band | 出现位置 | 出现次数 | 常见用途 | 高频误用 |
|---|---|---|---|---|
| Title | 第一页最上方 | 整份报表仅一次 | 报表大标题、Logo、打印时间 | 当成页眉,导致第二页无标题 |
| Page Header | 每页上部(首页在 Title 之下) | 每页一次 | 页码、创建人、系统名 | 把列名写在这里,分组后列名不跟组走 |
| Column Header | 每列的上部 | 每列一次 | 列名称 | 把明细数据放进来 |
| Detail | 正文区 | 每页重复 | 明细行,如收发货记录 | 高度设得过大,导致每页只排两三行 |
| Column Footer | 每列的下部 | 每列一次 | 列级小计、列说明 | 与 Page Footer 混用,小计重复累加 |
| Page Footer | 每页最下端 | 每页一次 | 页级小计、页码 X/Y | 高度残留导致末页空白 |
| Summary | 最后一页 Detail 之后 | 整份报表一次 | 全表合计、Crosstab 交叉表 | 放在其他区域,分页异常 |
| Group Header | 每个分组之前 | 每组一次 | 组说明、组标志字段 | 高度置 0 后又想显示组名 |
| Group Footer | 每个分组之后 | 每组一次 | 组内小计 | 未勾选重复打印,跨页后组尾丢失 |
| Last Page Footer | 最后一页最下端 | 仅最后一页 | 签收栏、审核人 | 与 Page Footer 同时启用,互相挤压 |
有个反直觉的点:Band 高度设为 0 并不等于这块区域被删除,它仍然参与布局计算,只是不占垂直空间。做快递面单时之所以敢把 Title、Page Header、Column Header 全设成 0,只留 Detail,靠的就是这个特性——区域还在,只是不显示。反过来,如果某个 Band 高度是 0 但里面还挂着会撑开的文本框(Stretch Type 设为 Stretch 之类),它依然会被内容顶开,这时候你把属性面板翻烂也找不到原因。
Band 的总高度还直接决定分页。页面可用高度 = pageHeight - 上边距 - 下边距,所有 Band 高度之和超过这个值,JasperReports 就会切页,于是出现「明明内容只有三行,却多出一张空白页」。遇到这种问题不用怀疑玄学,按顺序把 Column Footer、Page Footer、Summary 的高度逐个归零试一遍,两分钟就能定位。Detail 还有一个 Split Type 属性,控制明细行跨页时怎么断:Prevent 表示不拆行,整行挪到下一页,适合有边框的表格;Stretch 允许拉伸拆分,适合纯文本长内容。面单和票据类模板建议用 Prevent,避免一张单据被腰斩。
2.2 设计器布局与快捷键:把 Outline 和 Properties 用到顺手
Jaspersoft Studio 是基于 Eclipse RCP 构建的,所以它继承了一整套 Eclipse 的通用键位,熟悉 Java IDE 的人几乎零成本迁移。下面这几个是我每天都在用的,注意具体绑定以本机 keymap 为准,换过键位方案的可能对不上。
| 快捷键 | 作用 | 使用场景 |
|---|---|---|
| Ctrl + S | 保存当前模板 | 改完属性立刻存,避免编译时用的是旧文件 |
| Ctrl + Z / Ctrl + Y | 撤销 / 重做 | 拖错控件位置时救急 |
| Ctrl + C / V / X | 复制 / 粘贴 / 剪切 | 批量复制同一格式的 textField |
| Ctrl + Shift + F | 格式化源码 | 手改 jrxml 后对齐缩进,便于比对 diff |
| Ctrl + 鼠标滚轮 | 缩放设计区 | 面单这种小尺寸纸张必用 |
| Ctrl + M | 最大化当前视图 | 在 Outline 和设计器之间来回切 |
| F5 | 刷新 | 外部改了 JSON 数据文件后重新加载 |
| Ctrl + F | 查找替换 | 在 source 页签里批量改字段名 |
比快捷键更重要的是三个面板的用法。左下角的 Outline(报表大纲)是整份模板的骨架,Band 的增删、fields 和 variables 的管理、group 的创建全在这里右键完成,比在设计区点来点去稳得多。右下角的 Properties 是属性黑匣子,同一个控件它会根据当前选中的是 Band、是 textField 还是整份报表,切换出完全不同的属性集,找不到某个属性时先确认你选中的对象对不对。再就是底部的 Problems 视图,编译报错全靠它,很多人预览失败就反复点预览按钮,其实错误信息早就写在那里了。
预览和编译我一般不走快捷键:切到 Preview 页签,工具栏上直接点 PDF 或 HTML 图标,比记键位快。要注意预览用的是本地渲染,跟最终导出 PDF 走的不是同一条链路,这也是后面中文字体问题的根源。
2.3 自定义纸张与面单布局:Units 选 mm,非必要 Band 全部归零
快递面单、标签、小票这类场景,A4 那套页面格式完全用不上,必须走自定义纸张。操作路径是:新建完报表初始页面后,点页面最外框的外侧空白处选中整份 Report,在属性面板找到 Edit Page Format,Format 下拉选 Custom,然后在下方 Units 里选 mm,再填宽高和页边距。
这里有个必须说清楚的换算问题。Units 只是设计器的显示单位,jrxml 文件内部的坐标始终按像素存储,所以你把单位切到 mm 输入 100×150 之后,回到设计区看到的坐标数字会变大,那是正常的单位换算,不是设置丢了。反过来,如果你直接手改 jrxml 里的 pageWidth 和 pageHeight,就得自己算像素:px = mm ÷ 25.4 × 72。常用的几档我整理成表,省得每次掏计算器。
| 面单规格(mm) | jrxml pageWidth × pageHeight(px) | 适用场景 |
|---|---|---|
| 100 × 150 | 283 × 425 | 标准快递面单 |
| 100 × 180 | 283 × 510 | 带商品明细的面单 |
| 76 × 130 | 215 × 368 | 标签机小标签 |
| 210 × 99 | 595 × 281 | 三等分A4横版单据 |
页面尺寸定完,接着做减法。面单只保留 Detail 区域,其余 Band 高度全部设为 0,这一步在属性面板里逐个改,别漏 Page Footer——它是末页空白纸的第一嫌疑人。页边距也建议压到 0 或者 2mm 以内,面单本来就小,边距吃掉 10mm 就没地方放内容了。
<!-- 面单模板骨架:只留 Detail,其余区域高度归零 --> <jasperReport name="waybill_demo" pageWidth="283" pageHeight="425" columnWidth="283" leftMargin="0" rightMargin="0" topMargin="0" bottomMargin="0" whenNoDataType="AllSectionsNoDetail"> <title height="0"/> <pageHeader height="0"/> <columnHeader height="0"/> <detail> <band height="425"> <!-- 面单正文:条码、收件人、路由信息等控件放在这里 --> </band> </detail> <columnFooter height="0"/> <pageFooter height="0"/> <summary height="0"/> </jasperReport>这段骨架的关键在三个参数:pageWidth 和 pageHeight 用像素给值,leftMargin 到 bottomMargin 全压 0 保证内容贴边,whenNoDataType 设成 AllSectionsNoDetail 是为了数据为空时也能出一张空白面单(打印场景经常需要预打空白单)。Detail 的 band height 直接给成整页高度,是因为面单内容固定一页,不需要自动分页。如果面单里有条码控件,记得宽度要留够,条码在 PDF 里是按比例渲染的,容器窄了会被裁掉一半。
3. JSON 数据源落地:字段定义、中文 PDF 字体与类型对齐
Jasper 报表的数据源选择很灵活,JDBC、JavaBean、JSON、CSV 都能接。以 JSON 作为常用数据源时,标准顺序是:添加数据源(主要供预览用,没有数据源也能生成报表)、新建报表页、关联数据源、添加数据字段、添加并修改字段标签、属性设置、分组、预览、编译。这条链路看着长,真正卡人的只有两个点:字段类型对不上,和 PDF 里中文不见了。
3.1 从 JSON 文件到 fields:一份能直接跑的样例
先准备一份结构清晰的 JSON,字段名尽量跟数据库查询出来的列名保持一致,后面接 SQL 数据源时能省一轮重命名。嵌套层级不要超过两层,层级一深,fields 里的路径表达式就容易写错。
{ "rows": [ { "code": "RK202401001", "shipFromName": "某供应商", "closedAt": "2024-01-05 10:12:00", "itemCode": "IT-001", "itemName": "六角螺栓", "totalQty": 120, "openQty": 20, "warehouseCode": "WH01" }, { "code": "RK202401002", "shipFromName": "某贸易公司", "closedAt": "2024-01-05 15:40:00", "itemCode": "IT-002", "itemName": "平垫圈", "totalQty": 300, "openQty": 0, "warehouseCode": "WH02" } ] }数据源配置在 Data Adapters 里新建一个 JSON File 类型,选中文件,编码必须显式写成 UTF-8,否则中文会变成乱码。JSON Source 那一栏填数组节点的路径,上面这份就填 rows;如果整个文件本身就是数组,留空或者填一个点号即可。这个路径填错的表现很直接:预览时一行数据都不出,控制台提示找不到节点,比字段类型错误好排查得多。
在 Outline 里右键 fields 新建字段,把 JSON 里的每个键配一个 field,Name 必须跟键名逐字符一致,包括大小写。Class 按值的实际类型选:字符串给 java.lang.String,整数给 java.lang.Integer,小数给 java.math.BigDecimal,布尔给 java.lang.Boolean。日期这里要特别注意,JSON 里的 "2024-01-05 10:12:00" 是个字符串,如果直接选成 java.sql.Timestamp,填充时会抛类型转换异常,稳妥的做法是保持 String,在 textField 的 pattern 里用yyyy-MM-dd HH:mm做格式化输出。
报表里引用字段写成$F{code}、$F{shipFromName}这种形式。如果数据源不是平台托管的,而是自己在 Java 侧填充,代码大致是这样:
// 用 JSON 文件作为数据源填充并导出 PDF InputStream jsonStream = new FileInputStream("data/receipt.json"); // 第二个参数是数组节点路径,必须和 Data Adapter 里填的一致 JsonDataSource ds = new JsonDataSource(jsonStream, "rows"); Map<String, Object> params = new HashMap<>(); params.put("reportTitle", "入库商品明细表"); JasperPrint print = JasperFillManager.fillReport(compiledReport, params, ds); JasperExportManager.exportReportToPdfFile(print, "out/receipt.pdf");这段代码要说明两处:JsonDataSource 的第二个构造参数就是前面说的数组路径,两处不一致会出现「设计器里能预览、程序里跑出一张空表」的情况;fillReport 的第一个参数必须是编译后的 .jasper 文件,如果你改完 jrxml 没重新编译就直接跑 Java,用的还是旧模板,这种坑排查起来非常耗时间,我一般会在构建脚本里固定加一步预编译。
3.2 PDF 中文丢失的根因与 pdfStyle 全局配置
原始资料里有一句描述特别准确:不设置的话,报表预览里能显示中文,但上传到系统里字就没了。这不是玄学,是两条渲染链路用了不同的字体来源。设计器预览走的是本地 Java/Swing 渲染,用的是操作系统字体;导出 PDF 走的是 PDF 生成库,只有两种字体能用——PDF 标准 14 字体,或者显式嵌入的字体文件。中文不在标准字体里,没嵌入就是空白或者方框。
最省事的解法是设一个全局默认样式,让所有文本框自动带上中文 PDF 字体属性。在 jrxml 的 source 界面里,把 style 节点插到 jasperReport 根节点下、所有 band 之前:
<!-- 全局 PDF 样式:isDefault="true" 会作用到所有未单独指定样式的 textElement --> <style name="pdfStyle" isDefault="true" pdfFontName="STSong-Light" pdfEncoding="UniGB-UCS2-H" isPdfEmbedded="true"/>三个参数各管一段。pdfFontName 指定中文字体名,STSong-Light 是亚洲字体包里的宋体,兼容性最好;pdfEncoding 用 UniGB-UCS2-H,对应简体中文的 Unicode 编码,写成 GBK 之类在部分 PDF 阅读器里会缺字;isPdfEmbedded 设 true 表示把字体子集嵌进 PDF,文件会大一点,但换来的是在任何机器上打开都不掉字。
等效的另一种做法是在每个中文控件的 Advanced 面板里单独设,或者对单个 textElement 写<textElement pdfFontName="STSong-Light" pdfEncoding="UniGB-UCS2-H"/>。标签一多就很啰嗦,而且新加的控件容易漏,所以才更推荐全局 style。
这里有个边界必须提醒:从 6.x 版本开始,官方逐步用字体扩展(font extension)替代 pdfFontName 这套老属性,较新的 Studio 里这几个字段在属性面板中可能已经不可见。如果你的环境属于这种情况,走另一条路——在 Studio 的字体偏好里添加 TTF 字体文件(比如常见的开源中文字体),导出成 jar 放进 classpath 并在 jrxml 里引用字体名。这条路更重,但跨平台表现更稳。另外,就算设了 pdfStyle 还是空白,先检查 classpath 里有没有亚洲字体支持包(Maven 依赖 net.sf.jasperreports:jasperreports-fonts),缺了它 STSong-Light 根本加载不到,样式写得再对也没用。
3.3 漏字段、类型错、name 对不上:三条硬规则
用 JSON 数据源做报表,最常见的返工是字段漏了。原始资料给的做法很实在:在左下角报表大纲里右键 fields,手工新增所需字段,同时在属性框里重命名 Name 和描述,选择对应字段类型,注意 name 必须与数据库查询字段保持一致。
第一条硬规则是名称严格一致。Name 不只是给人看的标签,它是运行时的取值键,区分大小写,多一个下划线都取不到值。我习惯直接从 JSON 或 SQL 查询结果里复制字段名,绝不手敲。
第二条是类型选对。类型选错不会报错,只会静默出错,这是最恶心的地方。字符串类型的数值字段做 Sum 计算,结果是字符串拼接,合计栏会显示 "120 300" 这种鬼东西;日期字段类型选错,pattern 格式化不生效,显示成一串时间戳。
| JSON 值形态 | 应选 Class | 选错的典型表现 |
|---|---|---|
| "IT-001" | java.lang.String | — |
| 120 | java.lang.Integer | Sum 变字符串拼接 |
| 1.25 | java.math.BigDecimal | 金额精度丢失,出现 0.30000000000000004 |
| "2024-01-05 10:12:00" | java.lang.String + pattern | 格式化不生效,或填充时抛类型转换异常 |
| true / false | java.lang.Boolean | 条件打印判断永远为假 |
第三条是类型和控件要匹配。textField 的 Expression Class 要跟 field 的 Class 一致,把 String 字段拖进一个 Expression Class 为 Integer 的文本框里,编译能过,填充时会抛异常。批量检查的办法是切到 source 页签,用 Ctrl+F 搜class="java.lang.逐个比对,二十个字段也就两三分钟。
4. 分组、序号、交叉表:三个高频控件的参数怎么设
这三样东西几乎每个真实报表都会用到,也几乎每个新手都会在同一个地方卡住:分组后列名不跟组走、序号列永远是 1、交叉表拖进去一片空白。原因基本都出在几个隐藏属性上,代码本身没错。
4.1 Create Group 的两种做法与 band 高度置 0 的真实含义
分组的标准做法是在报表大纲里右键报表名,选 Create Group,在弹窗里选中分组字段。建完之后会多出 Group Header 和 Group Footer 两个区域,组内小计放 Footer,组说明放 Header。
第一套做法是针对「每组内容独立成页、但不需要显示组名」的报表。选中 Group Header 区域,在右下角属性面板的 band properties 里把 Height 设为 0,同时勾选 Start New Column 和 Start New Page 前面的复选框。效果是每个分组自动另起一页,组与组之间干净切分。
第二套做法是给「列名要跟着组走」的报表用的。直接把 Column Header 的 Height 设为 0,然后把 Column Header 中原来的所有列名控件全部拖到 Group Header 里,包括分组字段本身(可以放进来后隐藏显示)。这样分组后,每一组开头都会重新出现一次列名,视觉上就像按店铺、按仓库分册打印。
<!-- 按仓库分组:组头显示列名,组尾做小计,每组另起一页 --> <group name="byWarehouse" isStartNewPage="true" isReprintHeaderOnEachPage="true"> <groupExpression><![CDATA[$F{warehouseCode}]]></groupExpression> <groupHeader> <band height="24"> <textField> <reportElement x="0" y="0" width="60" height="20"/> <textFieldExpression><![CDATA[$F{warehouseCode}]]></textFieldExpression> </textField> </band> </groupHeader> <groupFooter> <band height="20"> <textField pattern="#,##0"> <reportElement x="180" y="0" width="80" height="18"/> <textFieldExpression><![CDATA[$V{totalQtySum}]]></textFieldExpression> </textField> </band> </groupFooter> </group>两个属性值得单独说。isStartNewPage 就是向导里那个 Start New Page 复选框,直接控制每组另起一页。isReprintHeaderOnEachPage 更容易被忽略:当一组明细多到跨页时,如果不勾这个,第二页就没列名了,打印出来一沓纸里有一半不知道哪列是哪列。做管理报表我默认把它打开。
还有个细节:把 Height 设为 0 只是让区域不占垂直空间,里面的控件仍然参与求值。所以我见过有人把列名拖进 Group Header 后忘了改 Height,结果每组页首白白空出一大块。改完高度一定切到 Preview 翻两页看看。
4.2 Create Variable 做序号列:resetType 与 incrementType 怎么选
明细行前面加个行号,视觉上能显著提升可读性,做核对和签字也方便。原始资料提到用报表大纲里的 Fx 功能,右键 Create Variable,在属性面板里重命名并设置相应参数,最后把变量拖到 Detail 区当第一列。具体参数怎么填,这里给一份能抄的。
右键 Fx → Create Variable,属性面板里按下面填:
| 属性 | 值 | 说明 |
|---|---|---|
| Name | rowNo | 变量名,报表里用$V{rowNo}引用 |
| Value Class | java.lang.Integer | 必须显式指定,留空会报错 |
| Calculation | Sum | 每求值一次就累加,配合下面的表达式实现自增 |
| Expression | 1 | 每次求值加 1 |
| Initial Value Expression | 0 | 分组重置时回到 0 |
| Reset Type | Report | 整份报表从 1 数到底;按页重置改成 Page |
| Increment Type | None | 不额外触发递增,交给 Detail 每行求值 |
关键在 Calculation 选 Sum、Expression 填 1 这个组合——它等价于「每遇到一条明细就加一」。Initial Value Expression 必须填 0 而不是留空,留空在部分版本里会得到 null,自变量后第一行变成 null 或直接报错。Reset Type 决定了序号在什么范围内连续:整份报表连续用 Report,每页从 1 开始用 Page,每个分组重新数用 Group 并在下面选具体分组名。
填完之后直接将变量拖拽到 Detail 区作为第一列。拖动时注意 Report Inspector 里的 Evaluation Time,保持 Auto 或者显式设为 Now;如果被设成了 Report,显示出来的会是总条数而不是行号——这个坑非常隐蔽,因为变量定义本身完全正确,只是求值时机不对。
顺带说两个内置变量。$V{REPORT_COUNT}是整份报表的明细计数,直接放到 Detail 里就是行号,懒得建变量时可以用它。$V{PAGE_NUMBER}做页码,但必须把 Evaluation Time 设成 Page,否则显示的永远是总页数;要显示「第 X 页 / 共 Y 页」,就得放两个 textField,一个 Evaluation Time 为 Now 显示当前页,一个为 Report 显示总页数。
4.3 Crosstab 交叉表:统计字段必须是 Integer,控件只能放 Summary
交叉表用来做二维汇总,比如按仓库和商品类别交叉统计收发数量。原始资料强调了两条约束,都是踩过坑才写下来的。
第一条,统计字段的类型。在导入数据源或手工新建 fields 时,凡是要参与交叉表统计的字段必须调整为 Integer 类型,例如明细里统计收发货绩效时,ReceiptCount 或 TotalQty 这两个字段从 JSON 读过来默认是 String,必须手动改成 int。不改的后果是交叉表里 Sum 计算变成字符串拼接,单元格里堆出一长串数字,看着像乱码。
第二条,Crosstab 只可存放于 Summary 区。把它拖到其他区域,某些场景下能渲染,但分页和重复渲染很容易出问题。所以做交叉表类的报表,设计第一步就是把多余的功能区域去掉,根据需要只保留 Title 和 Summary。
操作顺序是:从控件面板拖 Crosstab 到 Summary 区,选择引用已存在的数据源,在向导里按右上角的表格阴影提示依次选字段。左侧放行分组字段(比如仓库),上部放列分组字段(比如商品类别),然后是明细区,添加明细字段时把 Calculation 函数统一改成 Sum。完成后把 Crosstab 覆盖到整个界面,双击进入编辑态,深蓝色的 Static Text 就是行列标题,可以直接改成业务叫法。
最后一步九成人会忘:选中 Crosstab 里的全部明细字段,把字体大小改成 6 号并居中。默认字体对交叉表格子来说太大,列一多就挤成一片白,看起来像数据没出来,其实是文字被裁掉了。
5. 避坑与排查:五个翻车现场的现象、原因与解法
下面这五条都是我在实际模板里撞过的,按「现象 → 原因 → 解决」写,遇到类似的直接对号入座。
第一条,预览有中文,导出 PDF 全是方框或者干脆空白。
现象很固定:设计器里一切正常,切到 PDF 预览或者上传到系统后打印,中文字段整片消失。原因是 PDF 渲染链不吃操作系统字体,而模板里没有任何一个中文控件声明过 PDF 字体,导出时只能回退到标准字体,中文就没了。解决分三层:先在 jrxml 里加那条全局 pdfStyle;如果没效果,检查 classpath 里有没有亚洲字体支持包;再不行就说明当前版本走的是字体扩展路线,需要把 TTF 字体打包成 jar 引入,并在 textElement 里引用字体族名。别在单个控件上反复折腾字体名,先确认依赖到位。
第二条,序号列永远是 1,或者每页开头都从 1 重来。
现象是拖进去的 rowNo 变量,每一行显示的都是同一个值。三种原因对应三种改法:Calculation 被设成了 Nothing,变量只求值不累加,改成 Sum 并把 Expression 填 1;Reset Type 被设成了 Page 或 Column,导致每页重置,要整份连续就改成 Report;Evaluation Time 被设成 Report,显示的是最终值而不是当前行值,改回 Auto 或 Now。这三处任意一个出错表现都差不多,按这个顺序排查最快。
第三条,报表末尾莫名其妙多出一张空白页。
这个坑的血泪经验是:问题九成不在 Detail,而在你没注意的角落。常见原因有四个——Page Footer 或 Summary 的高度残留了十几像素;Detail 的 band height 比实际内容高出很多,最后一行排完刚好溢出;所有 Band 高度加上下边距超过了 pageHeight;以及空数据时默认行为是输出一张空页。解法是从下往上逐个把 Column Footer、Page Footer、Summary 高度归零验证,定位到具体哪个区域再调回去;面单类模板直接把非必要区域全设 0;空数据场景把 whenNoDataType 设为 AllSectionsNoDetail 或者 NoPages。
第四条,分组后每组另起一页,但列名不见了,或者组名被吃掉了。
现象有两种,原因恰好相反。列名不见,是因为把 Column Header 高度置 0 之后忘了把列名控件搬到 Group Header,列名跟着原区域一起消失了。组名被吃掉,是因为既要显示组名又把 Group Header 高度设成了 0,控件还在但没空间渲染。这两种需求本身互斥,动手之前先想清楚:要「每组带列名」还是「只要分页不要组头」,二选一,别指望一个配置同时满足。
第五条,SQL 条件写完,不填参数时查出一堆不相干的数据。
原始写法是AND (rh.code = :code OR rh.code IS NULL),本意是「参数为空时不过滤」,但实际语义变成了「单据号等于参数值,或者单据号本身是空的行」,结果是只要表里有 code 为 NULL 的记录,不管你填不填参数都会把它们捞出来。正确写法是把 NULL 判断加在参数上:AND (:code IS NULL OR rh.code = :code)。同一个模板里还有个方言问题:T-SQL 环境里写LIMIT 1会直接语法报错,要改成TOP 1或者用 OUTER APPLY。日期条件也别用<= :end,因为 :end 传进来的通常是当天 00:00:00,会丢掉当天的数据,用< DATEADD(DAY, 1, :end)才严谨。
6. 从模板到上线:打印配置 JSON、SQL 条件绑定与交付前自检
模板编译完只是半成品,在系统的打印模块里还得再配一层。以仓储系统为例,路径是「配置 → 打印配置 → 报表」,新增一条报表记录,填代码标识(建议和模板文件名保持一致,后面排查能少绕一圈)、描述、报表类型,然后写条件配置和 SQL 模板。
条件配置是个 JSON 数组,每个元素描述一个查询字段,决策字段四个:field、id、name、type。field 和 id 尽量保持一致,type 决定下一步 SQL 里怎么绑。对于作业报表,除 name 外基本不用改;管理报表要加起止日期、商品编码、供应商名这类自定义条件时,就得按类型补全。
[ {"field": "", "id": "h1_basic", "name": "基本信息", "type": "h1"}, {"field": "beg", "id": "beg", "name": "起始日期", "type": "date"}, {"field": "end", "id": "end", "name": "结束日期", "type": "date"}, {"field": "itemCode", "id": "itemCode", "name": "商品编码", "type": "string"}, {"field": "code", "id": "code", "name": "入库单号", "type": "string"}, {"field": "shipFromName", "id": "shipFromName", "name": "供应商名", "type": "string"} ]type 为 h1 的那条是分组标题,没有实际查询意义,保留即可。string 对应文本输入框,date 和 datetime 会出日期选择器,integer 出数字输入。字段名和 SQL 里的命名参数必须一一对应,差一个字母查询就恒为空,而且不报错,只是查不出数据。
SQL 模板按前面提的原则写,可选条件一律把 NULL 判断放在参数侧:
SELECT rh.closedAt, rd.receiptCode, rh.shipFromName, it.itemCategory1, it.code, it.name, rd.totalQty, rd.openQty, (rd.totalQty - rd.openQty) AS redQty, rd.warehouseCode, (SELECT TOP 1 ib.barcode FROM item_barcode ib WHERE ib.itemCode = rd.itemCode) AS BARCODE FROM RECEIPT_HEADER rh INNER JOIN RECEIPT_DETAIL rd ON rh.id = rd.receiptId INNER JOIN ITEM it ON rd.itemCode = it.code WHERE rh.leadingSts = 900 AND rh.trailingSts = 900 AND (:code IS NULL OR rh.code = :code) AND (:itemCode IS NULL OR rd.itemCode = :itemCode) AND (:shipFromName IS NULL OR rh.shipFromName = :shipFromName) AND (:beg IS NULL OR rh.closedAt >= :beg) AND (:end IS NULL OR rh.closedAt < DATEADD(DAY, 1, :end)) ORDER BY rh.closedAt DESC保存后回到列表上传模板。作业报表还要走一步「开发 → 菜单配置」,找到目标界面,在 View JSON 里打开对应加载项,在打印区域加上这条报表,文档名称填模板名、id 填打印配置里的代码名,点更新生效。管理报表不用挂菜单,直接在「管理 → 报表查询」里双击预览,选好期间和其他条件点 PDF 下载就能出结果。交付前我固定过一遍清单:模板名与代码标识一致、条件字段与 SQL 参数一一对应、PDF 中文已验证、末页无空白、日期边界用开区间。这套流程是从一次上线后才发现合计列错行开始的,从那以后每次上传前都强制走一遍,再没在这上面返工过。希望帮到你。
本文还有配套的精品资源,点击获取