🌐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 步驟萬用寫法」改,你的每個分頁都能在沒有資料時顯示一行「此版本無使用(分頁名)」且橫向合併到跟表頭一樣寬;有資料時則正常寫入並套上數字格式。
穩、清楚、可維護。下一位同事接手也不會崩潰。
留言
張貼留言