aol:sheet 一个模板生成多个工作表
适用模块:anyline-office / xlsx 模板渲染 · 标签:<aol:sheet>
1. 作用
aol:sheet 声明「当前工作表是模板」,引擎按数据源的元素个数把整张表复制若干份,
每份绑定自己的数据、起自己的表名。用于「数量由数据决定、每张表还要单独命名」的导出:
- 一个订单 + N 个商品:第 1 张表订单主体,后面每个商品一张表
- 按门店 / 客户 / 月份拆分,每个维度一张表
- 明细 + 分类出库单,每类一张表
标签写在工作表的任意一个单元格里(通常挑标题单元格或末尾备注单元格),它作用于整张工作表,不需要"标签体"包住内容。
只在 xlsx 生效
docx 没有多"页文本"的概念,该标签是 xlsx 专用。
2. 基本语法
<aol:sheet data="${goods}" var="g" name="${g.NAME}"></aol:sheet>
<!-- 自闭合同样可以 -->
<aol:sheet data="${goods}" var="g" name="${g.NAME}"/>
引擎在解析其它标签之前处理它:复制出来的工作表里还会继续解析 <aol:for>、<aol:if> 等标签。
3. 属性一览
| 属性 | 别名 | 默认 | 说明 |
|---|---|---|---|
data | d items is | ${items} | 数据源,集合 / JSON 数组 / 逗号分隔字符串 |
var | — | item | 迭代变量名,每份工作表绑定其中一条数据 |
status | — | status | 循环状态变量名,绑定 index/count/prev/next/first/last |
name | label title sheetName | 模板表名 | 表名模板,支持 ${},如 明细-${g.NAME};不写时用模板工作表自身的标签页名(见第 6 节) |
remove | rm | true | 是否删除模板工作表(见第 7 节) |
hide | — | false | remove="false" 时是否隐藏模板工作表 |
flatten | flat fl | false | 把当前数据的一级属性平铺进上下文,可直接写 ${字段} |
属性缺省值统一管理
sheet.xxx 没配时回落到 for.xxx(见 默认值说明),
例如 TagDefault.set(book.context(), "for.var", "row") 对 aol:sheet 一样生效。
4. 典型场景:订单 + N 个商品
模板文件只有 2 张工作表:
| 模板 sheet | 内容 |
|---|---|
| sheet1「订单」 | 订单主体 + 商品汇总列表(普通 ${order.NO} / <aol:for>) |
| sheet2「商品模板」 | 单元格里写一行 <aol:sheet data="${order.GOODS}" var="g" name="${g.NAME}"/>,其余是单个商品的版式 |
// Java 侧只需要给数据,不需要写任何"复制sheet"的代码
XWorkBook book = new XWorkBook(destFile);
book.variable("order", order); // order.GOODS 里有几个商品就生成几张表
book.save();
假如有 3 个商品,结果就是 订单 + 苹果 + 梨 + 香蕉(表名来自各条数据的 NAME,模板 sheet2 已从结果中删除)。
5. 每张表的数据从哪来
每张复制出来的工作表有一份独立上下文,里面预先放好:
var指定的变量(默认item):当前这条数据,如${g.NAME}status指定的状态 Map(默认status):index/count/prev/next/first/lastflatten="true"时再加当前数据的一级属性:${NAME}直接用- 工作簿级变量:
book.variable(...)/book.replace(...)设置的内容所有表都能用
表里再写明细循环时,引用各自的变量即可:
<!-- 商品模板里的规格明细:每个商品有自己的 specs -->
<aol:for data="${g.specs}" var="sp">${sp.NAME} ${sp.VALUE}</aol:for>
推荐写法
明细循环显式写
data="${g.xxx}",比依赖 flatten + 默认数据源更直观,也不容易因为列名同名误命中。
6. 表名规则
表名有两个来源,优先级从高到低:
| 优先级 | 来源 | 说明 |
|---|---|---|
| 1 | <aol:sheet name="…"/> | 写了就完全覆盖模板表自身的名称(以标签为准) |
| 2 | 模板工作表自身的标签页名 | Excel 里 worksheet 的名字;name 没写时用它,其中的 ${key} 同样会被解析 |
| 3 | SheetN | 两者都没有有效内容时的兜底,按顺序编号 |
两类来源都支持 ${key},并且每张表按自己的上下文渲染:
# 情况一:不写 name,用模板表自身的名称
模板 sheet 标签页名 : 商品-${item.NAME}
标签 : <aol:sheet data="${goods}"/>
结果 : 商品-苹果 / 商品-梨 / 商品-香蕉
# 情况二:写了 name,模板原名(XYZ)完全不参与
模板 sheet 标签页名 : XYZ
标签 : <aol:sheet data="${goods}" name="明细-${item.NAME}"/>
结果 : 明细-苹果 / 明细-梨 / 明细-香蕉
普通工作表(没有 aol:sheet 标签)自身的名称也一样会被解析:
模板 sheet1 标签页名: 订单-${order.NO} → 订单-SO-001 # book.variable("order", order)
模板 sheet4 标签页名: 日报-${DATE} → 日报-20261007 # book.replace("DATE","20261007")
${key} 的取值优先级:
| 优先级 | 来源 | 示例 |
|---|---|---|
| 1 | book.replace(k, v) 纯文本替换 | ${DATE} |
| 2 | 迭代变量 var | ${g.NAME} / 默认 ${item.NAME} |
| 3 | 循环状态 status | ${status.count} |
| 4 | 工作簿级 book.variable(k, v) | ${order.NO} |
| 5 | flatten="true" 平铺的一级属性 | ${NAME} |
同名 key
book.replace 先执行,同名占位符会被它先替换掉,EL 阶段不再有机会;迭代变量在本表上下文中覆盖同名的全局键。
渲染完成后再按 Excel 硬约束规范化:
| 规则 | 处理 |
|---|---|
非法字符 : \ / ? * [ ] | 替换成 _ |
| 超过 31 字符 | 截断到 31 字符 |
| 重名 | 自动加 (2) (3)…(不区分大小写) |
| 占位符取不到值 | 剔除残留的 ${…} 并打 warn 日志,不中断导出 |
| 渲染结果为空 | SheetN 兜底 |
渲染时机
表名在
parseTag() 之后统一渲染——此时每张复制表的上下文才绑定,所以 ${g.NAME} 能拿到各表自己的数据;
也因此表名里不能引用只有 <aol:for> 内部才存在的变量。
标签页顺序 = 数据顺序;模板工作表删掉后,结果就是「前面的普通表 + 按数据顺序排的复制表」。
7. 模板工作表自身的去留
| 写法 | 模板 sheet 的结局 |
|---|---|
默认 / remove="true" | 从结果中删除(最常用) |
remove="false" | 保留可见(数据源为空时也能留一张空表) |
remove="false" hide="true" | 保留但隐藏,Excel 标签页看不到 |
数据源为空
数据为空时一份都不会复制,此时
remove="false" 能保住这张模板表;
如果所有工作表都不可见,引擎会自动补一张空白表,避免生成 Excel 打不开的文件。
8. 底层 API(硬编码方式)
标签只是上层驱动方式,底层这几个方法可以直接调用(业务代码里拼,不走模板):
| 方法 | 说明 |
|---|---|
book.copy(XSheet tpl, String label) | 按模板复制一张新表并命名,label 支持 ${key} |
book.createSheet(String label) | 新建空白可见表,label 支持 ${key}(按工作簿上下文渲染) |
book.remove(XSheet) | 删除表(同步清理 workbook.xml / rels / Content_Types / zip 部件) |
book.hide(XSheet) | 隐藏表 |
book.sheetName(String) | 表名规范化(截断 / 替换非法字符 / 去重) |
book.rename(XSheet, String) | 改已有的表名(回写 workbook.xml) |
sheet.variable(k, v) | 只在这张表生效的变量 |
XSheet tpl = book.sheet(1);
for(Goods g : order.getGoods()){
XSheet sheet = book.copy(tpl, g.getName());
sheet.variable("goods", g);
}
book.remove(tpl);
注意 sheet.variable() 第一次调用时会克隆一份上下文,之后 book.variable() 新加的变量不会再进这张表——所以公用变量请在前面设置。
9. 常见坑
- 跨表公式不跟随:
=SUM(商品1!F9:F11)这种引用表名的公式,复制后表名变了但公式不会改,会失效。模板里请只用本表内的公式。 - 命名区域(definedName)不会同步:打印区域、
localSheetId相关的定义复制后可能错位,建议让第 1 张表保留原有区域。 - 一张表一份 DOM:表越多内存占用越大,几百张以上建议分批导出成多个文件。
- 标签要写在单元格里:它是靠扫描单元格文本识别的;同一个单元格里可以跟别的文字共存(标签本身会被摘掉)。
- 一张模板表只认一个
aol:sheet:同一张工作表里写多个,只有第一个生效。 - 模板表的图片/图形:会连同 drawing 部件一起复制;
aol:img这类运行时插入的图片请在每张表里各自配置数据源。 - 表名以标签为准:
<aol:sheet name="…">写了之后,模板表自身的标签页名不再参与;反过来标签不写name,才会去用模板表名。 - 至少留一张可见表:删除操作会拦住"把可见表删光"的情况,并自动补一张空白表。