🌐Vue 3 + SheetJS 匯出多分頁 Excel:一次搞懂「變數未定義」與「合併儲存格」的坑
為什麼會寫這篇?
我們在一個 Vue 3 專案裡,用
SheetJS (xlsx) 把多個 API
回傳的資料匯出成
多分頁 Excel。每個分頁都有自訂雙層表頭、欄寬、數字格式;如果某個分頁「沒有資料」,就要在標題下方放一行文字(例如:此版本無使用SupplyPegLog),而且那一行要橫向合併到和表頭一樣寬。
結果遇到兩個經典地雷:
-
ReferenceError: wsThree is not defined -
wsX["!merges"].push(...)時崩掉,因為!merges沒被初始化
以下用工程師視角,白話講給不熟的人看,順便給出可直接複用的寫法。
背景結構(簡述)
-
Vue 3 + Vite
-
Pinia 管版本參數
-
Axios 打
/api/Data/... -
SheetJS 建立 Workbook → 為每張表建立
Worksheet→ 設定合併、欄寬、格式 →book_append_sheet→writeFile
問題 1:ReferenceError(變數作用域)
症狀:主控台顯示
ReferenceError: wsThree is not defined
常見原因:你在
if/else 的
else 區塊裡 才宣告了
const wsThree = ...,但在區塊外又去使用 wsThree(例如
book_append_sheet(wb, wsThree, ...)
或在 if 分支裡 push merges)。
JS 的 const/let
有區塊作用域,離開那個
{} 就看不到了。
修法重點:
先在 if/else 之外建立
wsThree,再在分支內寫入資料或放空訊息。
問題 2:!merges 未初始化
症狀:當資料為空時,你會做:
wsX["!merges"].push({ s:{ r:2, c:0 }, e:{ r:2, c:headerRow.length - 1 } })
但很多分頁(例如 wsThree/wsFour…)沒有先設定
wsX["!merges"],導致
push 時炸掉。你可能只有在
2.RemainSupply 那頁先賦值了
!merges,其他頁沒有。
修法重點:
每張表在建立之後,一定先做
wsX["!merges"] = [],接著再 push 要合併的範圍。資料為空要顯示訊息時,也是在這個陣列裡加一個合併設定。
萬用寫法(建議遵循的 5 個步驟)
以任何一張分頁為例,流程都是:
-
建立表頭:
const ws = XLSX.utils.aoa_to_sheet([headerRow1, headerRow2, ...]) -
初始化合併:
ws["!merges"] = [] -
加入表頭需要的合併範圍(如果你的第一列、第二列要跨欄)
-
有資料 →
sheet_add_json;沒資料 →sheet_add_aoa([["此版本無使用XXX"]], { origin: "A3" })+!merges.push(...) -
append 到活頁簿:
XLSX.utils.book_append_sheet(wb, ws, "工作表名稱")
範例:3.SupplyPegLog(可直接貼用的骨架)
// 1) 表頭
const headerRow31 = [
"From APS Input", "", "", "", "", "",
"APS Kitting 過程中物料分配到哪些需求中了", "", "", "", "", "", "", ""
];
const headerRow32 = [
"SITE","ORG","ITEM","SUPPLY_DATE","SUPPLY_QTY","SUPPLY_TYPE",
"SEQ_NO","Site","AppL","Customer","PRODUCT_ID","MODEL","PEG_Date","PEG_QTY"
];
// 2) 建立 sheet 並初始化 merges
const wsThree = XLSX.utils.aoa_to_sheet([headerRow31, headerRow32]);
wsThree["!merges"] = [];
// 3) 表頭合併
wsThree["!merges"].push(
{ s: { r:0, c:0 }, e: { r:0, c:5 } }, // A1:F1
{ s: { r:0, c:6 }, e: { r:0, c:13 } } // G1:N1
);
// 欄寬(可選)
wsThree["!cols"] = [
{ wch:12 },{ wch:8 },{ wch:30 },{ wch:15 },{ wch:15 },{ wch:18 },
{ wch:20 },{ wch:12 },{ wch:12 },{ wch:20 },{ wch:20 },{ wch:20 },
{ wch:15 },{ wch:15 }
];
// 4) 依資料量處理
if (dataThree.length === 0) {
XLSX.utils.sheet_add_aoa(wsThree, [["此版本無使用SupplyPegLog"]], { origin: "A3" });
// A3 到 N3(headerRow32 長度 = 14)
wsThree["!merges"].push({ s: { r:2, c:0 }, e: { r:2, c: headerRow32.length - 1 } });
} else {
XLSX.utils.sheet_add_json(wsThree, dataThree, { skipHeader: true, origin: "A3" });
// 數字欄位格式化(E=SUPPLY_QTY, N=PEG_QTY)
const range = XLSX.utils.decode_range(wsThree["!ref"]);
for (let r = range.s.r + 2; r <= range.e.r; r++) {
[4, 13].forEach(c => {
const cell = wsThree[XLSX.utils.encode_cell({ r, c })];
if (cell && cell.t === "n") cell.z = (cell.v % 1 !== 0) ? "#,##0.0000" : "#,##0";
});
}
}
// 5) 加進活頁簿
XLSX.utils.book_append_sheet(wb, wsThree, "3.SupplyPegLog");
只要把「標題列、欄位數量、要合併到哪一欄」換成各分頁的設定,就能套用在
4.DemandPegLog、6.新產品、7.策備、8.物料限制、9.模具、10.供應商產能。
2.RemainSupply 的特例
你的
2.RemainSupply
原本就做對了:先建立
wsTwo、設定表頭合併,再在沒有資料時加上
其他分頁也要比照這個流程,差別只是表頭長度與合併範圍不同。
快速檢查清單(你一定會用到)
-
每張表
wsX建立後立刻:wsX["!merges"] = [] -
表頭合併範圍先
push進!merges -
if (data.length === 0)→sheet_add_aoa([...], { origin: "A3" })+ 合併A3到headerRow.length尾欄 -
else→sheet_add_json,skipHeader: true,再做數字欄位格式化 -
最後才
book_append_sheet -
變數不要只在
else裡宣告;在區塊外先建立wsX(避免ReferenceError)
常見小坑再提醒
-
headerRow.length - 1是0-based 欄位索引,別寫錯。 -
decode_range(ws["!ref"])取得範圍後,資料列起點是 A3 ⇒ 索引 = 2。 -
cell.t === 'n'才能套數字格式;字串不會生效。 -
sheet_add_json的origin: "A3"會把資料從第三列開始寫,剛好避開雙層表頭。 -
若 API 回傳的 key 跟 Excel 欄位不一致,可用你現成的
normalizeRow先轉,再sheet_add_json。
總結
這次踩雷的本質其實只有兩件事:
-
變數作用域:
wsX要在 if/else 外就建好,否則會is not defined。 -
陣列初始化:每張表的
wsX["!merges"]先設成空陣列,再push合併範圍;資料為空時也一樣。
照著上面的「5 步驟萬用寫法」改,你的每個分頁都能在沒有資料時顯示一行「此版本無使用(分頁名)」且橫向合併到跟表頭一樣寬;有資料時則正常寫入並套上數字格式。
穩、清楚、可維護。下一位同事接手也不會崩潰。
留言
張貼留言