Word 文件模組使用指南
Package:
io.leandev.appfuse.document.*狀態: 穩定(v1) 格式: Office Open XML(.docx)
簡介
AppFuse Document 提供以 .docx 範本為起點的 Word 文件產生 API,封裝 Apache POI(XWPF),讓應用層不直接接觸 POI/OOXML 型別。設計決策與取捨見 ADR-008。
核心特色
| 特色 | 說明 | 價值 |
|---|---|---|
| 範本錨定 | 載入既有 .docx,在其上編排 | 版面/樣式由設計師維護的範本決定 |
| locator 定位 | findParagraphByText("[x]") → Optional | 無隱藏游標狀態、可組合 |
| 共用容器面 | 本文/頁首/頁尾/儲存格同一套 Body 操作 | 一致、好學 |
| HTML 富文本 | 把 RichTextEditor 的 HTML 渲染進 Word | 表單富文本直接落地公文 |
| 依賴隔離層 | POI/OOXML 不洩漏到公開 API | 升級/替換底層不影響應用層 |
範圍與限制(v1)
| 支援 | 規劃中(follow-up) |
|---|---|
範本載入/輸出、段落/run/表格/列編排、頁面章節、頁首頁尾內容、圖片、頁碼欄位、appendCopyOf、HTML 富文本(文字流 + 圖片) | HTML 內嵌 <table> 渲染、清單自動編號定義、文件合併、PDF 匯出、樣式定義建立、儲存格內新建巢狀表格 |
富文本輸入為 appfuse-web
RichTextEditor(Tiptap)的 HTML 字串,非舊版rtxAST。
快速開始
範本套印(最常見)
import io.leandev.appfuse.document.*;
try (InputStream template = getClass().getResourceAsStream("/templates/certificate.docx");
Document doc = Document.open(template);
OutputStream os = response.getOutputStream()) {
// 依占位文字定位並填值
doc.body().findParagraphByText("[customer]").ifPresent(p -> p.setText(customer.getName()));
doc.body().findParagraphByText("[date]").ifPresent(p -> p.setText(today));
// 或整份本文取代占位字串(跨段落、跨儲存格)
doc.body().replaceText("[certNo]", cert.getNo());
doc.write(os);
}
表格逐筆填(line items)
Table items = doc.body().tables().get(0);
for (OrderLine line : order.getLines()) {
TableRow row = items.appendRow();
row.cell(0).setText(line.name(), "answerStyle"); // 第二參數為樣板定義的樣式名
row.cell(1).setText(line.amount());
}
樣板表格複製多份(appendCopyOf)
// 以範本中既有的第一個表格為原型,每條產線複製一份再各自填值
Table prototype = doc.body().tables().get(0);
for (ProductionLine pl : productionLines) {
Table copy = doc.body().appendCopyOf(prototype);
copy.cell(0, 0).setText("產線:" + pl.getName());
}
從零建立
try (Document doc = Document.create()) {
doc.setPageSize(Paper.A4);
doc.body().appendParagraph("標題", "Heading1");
doc.body().appendParagraph("內文段落");
doc.write(os);
}
定位(locator)
不使用有狀態游標;定位即取得可編輯的 handle。
Body body = doc.body();
body.findParagraphByText("[name]"); // Optional<Paragraph>,部分文字比對
body.findParagraphByBookmark("sign_here"); // Optional<Paragraph>,依書籤
body.findCellByText("[total]"); // Optional<TableCell>
body.findCellByBookmark("amount"); // Optional<TableCell>
Body 是本文、頁首、頁尾、儲存格的共用容器面——以上方法在四者皆可用。
段落與文字格式
Paragraph p = doc.body().appendParagraph();
p.setText("重要");
p.setStyle("Heading2"); // 樣板定義的樣式名(style ID)
p.setAlignment(Alignment.CENTER);
p.setIndent(Length.ofCentimeter(1));
p.setSpacingAfter(Length.ofPoint(6));
Run run = p.addRun("加粗紅字");
run.setBold(true);
run.setColor(java.awt.Color.RED); // 顏色用 java.awt.Color
run.setFontSize(16); // 字級用 point
run.setUnderline(true);
p.addPageNumber(); // 頁碼欄位(常用於頁尾)
表格
Table table = doc.body().appendTable(3, 2); // 3 列 2 欄
table.cell(0, 0).setText("標題");
table.row(1).cell(0).setText("資料");
table.appendRow().cell(0).setText("新列");
table.duplicateRow(1); // 複製第 1 列(含內容格式)插入其後
table.removeRow(2);
table.mergeCellsVertically(0, 0, 2); // 第 0 欄、第 0–2 列垂直合併
table.mergeCellsHorizontally(0, 0, 1); // 第 0 列、第 0–1 欄水平合併
table.mergeRegion(0, 0, 1, 1); // 矩形區塊合併
table.setFullWidth(); // 滿版
table.setColumnWidth(0, Length.ofCentimeter(4));
TableCell cell = table.cell(0, 0);
cell.setVerticalAlignment(VerticalAlignment.CENTER);
cell.appendParagraph("儲存格內可再放多個段落"); // cell 本身即 Body 容器
頁首 / 頁尾
頁首頁尾的編輯面與本文相同(皆為 Body)。
Body footer = doc.footer(); // 預設頁尾(不存在則建立)
Paragraph p = footer.appendParagraph();
p.setAlignment(Alignment.CENTER);
p.addRun("第 ");
p.addPageNumber();
p.addRun(" 頁");
doc.firstFooter(); // 首頁頁尾
doc.header(); // 預設頁首
頁面與章節
doc.setPageSize(Paper.A4);
doc.setOrientation(Orientation.LANDSCAPE);
doc.setMargins(Length.ofCentimeter(2.5), Length.ofCentimeter(2),
Length.ofCentimeter(2.5), Length.ofCentimeter(2));
doc.setMarginFooter(Length.ofCentimeter(1));
doc.addPageBreak();
doc.addSectionBreak();
doc.restartPageNumber(1);
尺寸一律用框架的
io.leandev.appfuse.measure.Length/Paper。
HTML 富文本
把 appfuse-web RichTextEditor(Tiptap/ProseMirror)輸出的 HTML 字串渲染進 Word。
// 場景 A:占位段落替換成富文本
doc.body().findParagraphByText("[content]")
.ifPresent(p -> p.replaceWithHtml(form.getRemarkHtml()));
// 場景 B:末端附加
doc.body().appendHtml(html);
| HTML 子集 | → Word |
|---|---|
strong/em/u/s/code/mark/sup/sub、span style=color | run 格式 |
p、h1–3、blockquote、pre | 段落/標題/樣式 |
ul/ol/li | 清單(前綴項目符號/編號) |
text-align | 段落對齊 |
img(base64) | 內嵌圖片 |
table | ⏸ v1 不渲染(結構化表格請用原生 Table API) |
圖片
Run run = doc.body().appendParagraph().addRun("");
run.addImage(inputStream, Length.ofCentimeter(3), Length.ofCentimeter(3)); // 串流
run.addImage(bufferedImage, Length.ofCentimeter(3), Length.ofCentimeter(2)); // BufferedImage(編碼為 PNG)
常見問題
Q: 為什麼不直接用 Apache POI?
A: 提供穩定的隔離 API、不洩漏 POI/OOXML 型別,與 csv/workbook 同策略;底層升級或替換不影響應用層。
Q: 占位符要用什麼語法?
A: 框架不規定。範本作者自選([x]、{{x}} 皆可),以 findParagraphByText / replaceText 比對該字串即可。
Q: 樣式(setStyle("answerStyle"))的名字哪裡來?
A: 範本 .docx 內定義的樣式 ID。範本套印時引用範本既有樣式;v1 不支援以程式新建樣式定義。