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. 属性一览

属性别名默认说明
datad items is${items}数据源,集合 / JSON 数组 / 逗号分隔字符串
var—item迭代变量名,每份工作表绑定其中一条数据
status—status循环状态变量名,绑定 index/count/prev/next/first/last
namelabel title sheetName模板表名表名模板,支持 ${},如 明细-${g.NAME};不写时用模板工作表自身的标签页名(见第 6 节)
removermtrue是否删除模板工作表(见第 7 节)
hide—falseremove="false" 时是否隐藏模板工作表
flattenflat flfalse把当前数据的一级属性平铺进上下文,可直接写 ${字段}
属性缺省值统一管理 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/last
  • flatten="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} 同样会被解析
3SheetN两者都没有有效内容时的兜底,按顺序编号

两类来源都支持 ${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} 的取值优先级:

优先级来源示例
1book.replace(k, v) 纯文本替换${DATE}
2迭代变量 var${g.NAME} / 默认 ${item.NAME}
3循环状态 status${status.count}
4工作簿级 book.variable(k, v)${order.NO}
5flatten="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. 常见坑

  1. 跨表公式不跟随:=SUM(商品1!F9:F11) 这种引用表名的公式,复制后表名变了但公式不会改,会失效。模板里请只用本表内的公式。
  2. 命名区域(definedName)不会同步:打印区域、localSheetId 相关的定义复制后可能错位,建议让第 1 张表保留原有区域。
  3. 一张表一份 DOM:表越多内存占用越大,几百张以上建议分批导出成多个文件。
  4. 标签要写在单元格里:它是靠扫描单元格文本识别的;同一个单元格里可以跟别的文字共存(标签本身会被摘掉)。
  5. 一张模板表只认一个 aol:sheet:同一张工作表里写多个,只有第一个生效。
  6. 模板表的图片/图形:会连同 drawing 部件一起复制;aol:img 这类运行时插入的图片请在每张表里各自配置数据源。
  7. 表名以标签为准:<aol:sheet name="…"> 写了之后,模板表自身的标签页名不再参与;反过来标签不写 name,才会去用模板表名。
  8. 至少留一张可见表:删除操作会拦住"把可见表删光"的情况,并自动补一张空白表。