用 Node.js 批次產生 PowerPoint 報告

本頁目錄

程式碼:examples/node/pptx-report/

問題

測試資料整理完了,但主管要的是投影片。每次都要手工把表格貼進 PowerPoint、調欄寬、換色、加標題——一次二十分鐘,一個月做四次。

解法

pptxgenjs 純 JavaScript,不需要裝 PowerPoint,Windows / Linux / CI 都能跑。

npm init -y && npm pkg set type=module && npm i pptxgenjs
import PptxGenJS from "pptxgenjs";

const pptx = new PptxGenJS();
pptx.layout = "LAYOUT_16x9";           // 10 x 5.625 英吋

// 定義母片,標題列與頁尾只寫一次
pptx.defineSlideMaster({
  title: "MAIN",
  background: { color: "FFFFFF" },
  objects: [
    { rect: { x: 0, y: 0, w: "100%", h: 0.55, fill: { color: "1F3864" } } },
    { text: {
        text: "進料檢驗月報",
        options: { x: 0.35, y: 0, w: 6, h: 0.55, color: "FFFFFF",
                   fontSize: 16, bold: true, valign: "middle" },
    }},
  ],
  slideNumber: { x: 9.3, y: 5.25, color: "888888", fontSize: 10 },
});

const slide = pptx.addSlide({ masterName: "MAIN" });
slide.addText("2026 Q1 不良率彙總", { x: 0.35, y: 0.8, fontSize: 24, bold: true });

await pptx.writeFile({ fileName: "out/月報.pptx" });

表格:第一列是表頭

addTable 吃的是二維陣列,每格可以是字串或帶樣式的物件。

const header = ["供應商", "批數", "不良批", "不良率"].map((t) => ({
  text: t,
  options: { bold: true, color: "FFFFFF", fill: { color: "1F3864" }, align: "center" },
}));

const body = rows.map((r) => [
  r.supplier,
  { text: String(r.lots), options: { align: "right" } },
  { text: String(r.ng), options: { align: "right" } },
  { text: `${(r.rate * 100).toFixed(1)}%`,
    options: { align: "right",
               color: r.rate > 0.05 ? "C00000" : "333333",   // 超標標紅
               bold: r.rate > 0.05 } },
]);

slide.addTable([header, ...body], {
  x: 0.35, y: 1.4, w: 9.3,
  colW: [3.3, 2.0, 2.0, 2.0],
  fontSize: 12,
  border: { type: "solid", color: "D9D9D9", pt: 0.5 },
  rowH: 0.32,
  valign: "middle",
});

圖表

pptxgenjs 產的是原生 PowerPoint 圖表,不是圖片——收到的人可以點進去改資料、換樣式。

slide.addChart(pptx.ChartType.bar, [{
  name: "不良率",
  labels: rows.map((r) => r.supplier),
  values: rows.map((r) => +(r.rate * 100).toFixed(2)),
}], {
  x: 0.35, y: 1.4, w: 9.3, h: 3.6,
  barDir: "col",
  chartColors: ["4472C4"],
  showValue: true,
  valAxisTitle: "不良率 (%)",
  showLegend: false,
});

一份資料一頁,批次跑

for (const [supplier, records] of Object.entries(grouped)) {
  const s = pptx.addSlide({ masterName: "MAIN" });
  s.addText(supplier, { x: 0.35, y: 0.8, fontSize: 22, bold: true });
  s.addTable(buildTable(records), { x: 0.35, y: 1.4, w: 9.3, fontSize: 11 });
  s.addNotes(`資料期間 ${period},共 ${records.length} 批。`);   // 講稿備忘稿
}

完整版(讀 CSV、分組、封面頁、圖表頁)在 build-report.mjs

node build-report.mjs --data sample-data.csv --out out/月報.pptx

踩過的坑

單位是英吋不是像素。 LAYOUT_16x9 是 10 × 5.625 英吋。x: 100 會把物件丟到畫面外一百英吋處,而且不會報錯——你只會得到一張空白投影片。座標一律用小數。

中文字型要明確指定。 不指定的話在對方電腦上會 fallback 成 Calibri,中文變成方框或明體。全域設定:

pptx.theme = { bodyFontFace: "微軟正黑體", headFontFace: "微軟正黑體" };

給外部客戶的話用「Microsoft JhengHei」,英文名在非中文版 Office 上比較保險。

writeFile 是 async,一定要 await。 少了 await,Node 可能在檔案寫完前就退出,你會得到一個 0 byte 或損毀的 .pptx

輸出目錄不存在不會自動建立。 writeFile 直接丟 ENOENT。先 await fs.mkdir(dirname(out), { recursive: true })

表格太長不會自動分頁。 超過投影片高度的部分直接被切掉,一樣不報錯。自己算:一頁大約放 12–14 列(rowH: 0.32),超過就分頁:

const PER_PAGE = 12;
for (let i = 0; i < body.length; i += PER_PAGE) {
  const s = pptx.addSlide({ masterName: "MAIN" });
  s.addTable([header, ...body.slice(i, i + PER_PAGE)], { /* ... */ });
}

顏色不要加 # color: "#C00000" 會被當成無效值靜靜忽略,寫 "C00000"


回到頂端

MIT License — 範例程式可自由取用。

This site uses Just the Docs, a documentation theme for Jekyll.