🌐Vue 3 匯出 Excel 筆數變少、資料重複?— 工程師一步步帶你找出篩選與合併的地雷(含完整修正範例)
內容
TL;DR:
匯出的 Excel 筆數變少/重複,通常不是你「少抓資料」,而是被篩選過或不小心把行合併了。三大元凶:
匯出用
filteredData而非完整的mappedTableData;Pinia 的全域篩選殘留,初始化順序錯亂;
表頭鍵名對不上(中英文映射)+合併
merges/rowSpans把不同列「壓成一列」。
先用無合併、無格式化的純匯出驗證筆數,確認 OK 再逐步加回樣式與合併。
一、問題現象(非技術也看得懂)
我們要把「NB急單計劃排程結果 - 考題結論」這張表匯出到 Excel。結果下載後發現:
-
筆數變少:明明畫面上很多筆,Excel 檔卻只有一小部分。
-
資料重複:同樣的計劃編號/序號反覆出現,好像被「擠在一起」。
-
與參考檔案不一致:跟 inx0728(內部參考檔)比對,欄位名稱與筆數都對不起來。
二、背景架構(快速理解)
-
前端:Vue 3 + Pinia 管理狀態,頁面有搜尋關鍵字與多欄位下拉篩選。
-
匯出:用 SheetJS (XLSX) 產生 Excel,還做了表頭群組與儲存格合併(
merges)。 -
欄位:畫面顯示用中文標題(例如
SEQ_NO序號、APS_PLAN_NO計劃編號),但原始資料是英文鍵名(例如SEQ_NO、APS_PLAN_NO)。
關鍵:匯出的資料來源、表頭鍵名映射、合併邏輯,三者要完全對齊,否則就會「看起來像少筆或重複」。
三、三大根因(工程師視角拆解)
1) 匯出用
filteredData 導致筆數變少
匯出程式是這樣寫的:
const dataToExport = filteredData.value
filteredData
是「套用搜尋與篩選後的結果」。
只要有殘留過濾(包含你上一頁留下來的 Pinia 全域篩選),匯出就只剩一部分。
✅ 修正:若要完整匯出,不吃篩選,改成:
const dataToExport = mappedTableData.value
2) Pinia 全域篩選殘留/初始化順序錯誤
你會把每頁的常用欄位篩選存在 Pinia(globalCommonFilters)。若先清空或先賦值的順序不對,initUniqueAndFilters()
會以為「篩選值是空」,結果所有列都被過濾掉。
✅ 修正:
-
先用實際資料的 unique 值建立初始篩選,再把它存回 Pinia。
-
或在切版本/重查前,明確清掉全域篩選,避免前一頁的條件外溢。
3) 表頭鍵名映射 + 合併邏輯 讓資料「被擠扁」
-
你的
tableHeaders來自Object.keys(uppercaseMapping)(中文展示用),但原始資料鍵名來自後端(英文)。一旦對不上,某些欄位就變空,之後又用rowSpans/merges依值合併,不同列就被誤判一致 → 看起來像重複或少筆。 -
merges若以區段/值合併但鍵名不對、或排序/分組鍵不完整,也會「壓扁資料」。
✅ 修正:
-
以後端回傳的實際鍵名動態生成
tableHeaders。 -
先不要做合併,確認筆數與內容正確後再慢慢加回。
-
合併前,務必確認分組鍵(如
APS_PLAN_NO、SEQ_NO)與排序一致。
四、逐步排查(1 次只做 1 件事)
-
加日誌先看筆數
console.log('mappedTableData:', mappedTableData.value.length) console.log('filteredData:', filteredData.value.length)如果
filteredData<mappedTableData,代表「被篩選了」。 -
停用篩選後匯出
暫時改成匯出mappedTableData,看看筆數是否回到正確。 -
停用合併與格式
先用純匯出驗證:import * as XLSX from 'xlsx' const ws = XLSX.utils.json_to_sheet(mappedTableData.value) const wb = XLSX.utils.book_new() XLSX.utils.book_append_sheet(wb, ws, '考題結論') // save...
如果此時筆數正確,表示「篩選/表頭/合併」其中某一步是兇手。
-
對齊欄位名稱與順序
依參考檔(例如 inx0728)定義 header 順序,避免欄位丟失:const headerOrder = ['APS_VERSION','APS_PLAN_NO','DC_ORDER_NO','DOC_NO', /* ... */] const ws = XLSX.utils.json_to_sheet(mappedTableData.value, { header: headerOrder }) -
最後才加回合併
每加一段merges就重匯一次,確認筆數沒被影響。
五、建議修正:關鍵片段(可直接套)
(A) 以「資料實際欄位」建立表頭
// 拿到 res.data 後
const rows = res.data.map(item => {
const o = {}
Object.keys(item).forEach(k => { o[k.toUpperCase()] = item[k] })
return o
})
// 以資料實際鍵建立 header(避免硬卡在映射表)
tableHeaders.value = Object.keys(rows[0] || {}).map(k => k.toUpperCase())
(B) 初始化篩選:先建、後存
filters.value = {}
// 先跑 unique
tableHeaders.value.forEach(hdr => {
const colVals = rows.map(r => r[hdr])
uniqueValues.value[hdr] = Array.from(new Set(colVals))
})
// 再設 filters 初值
tableHeaders.value.forEach(hdr => {
filters.value[hdr] = [...uniqueValues.value[hdr]]
})
// 最後存回 Pinia(避免空白覆蓋)
tableHeaders.value.forEach(hdr => {
if (commonFilterKeys.includes(hdr)) {
store.setGlobalCommonFilter(hdr, filters.value[hdr])
}
})
(C) 匯出一開始用「完整資料」
function exportToExcel() {
if (!mappedTableData.value.length) {
alert('沒有資料可匯出')
return
}
console.log('mapped:', mappedTableData.value.length)
console.log('filtered:', filteredData.value.length)
// 先用完整資料驗證筆數
const dataToExport = mappedTableData.value
const ws = XLSX.utils.json_to_sheet(dataToExport)
const wb = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(wb, ws, '考題結論')
// 後續再加格式與 merges
}
(D) 加回指定順序與中文表頭(可選)
const headerOrder = ['APS_VERSION','APS_PLAN_NO','SEQ_NO', /* ... */]
const humanReadableMap = {
APS_VERSION: '版本',
APS_PLAN_NO: '計劃編號',
SEQ_NO: '序號',
// ...
}
// 建表頭
const ws = XLSX.utils.json_to_sheet(
dataToExport.map(row => {
const o = {}
headerOrder.forEach(k => o[humanReadableMap[k] || k] = row[k])
return o
}),
{ header: headerOrder.map(k => humanReadableMap[k] || k) }
)
(E) 合併前的檢查原則
-
合併只用在「同一欄位,同一群組,值完全一致、且確定同筆資料需要視覺合併」的情況。
-
你的分組鍵(例如
APS_PLAN_NO、SEQ_NO)要先排序再逐段合併,避免跨組誤合。 -
鍵名必須對齊資料,不要用展示用(中文)鍵合併。
六、對照檢查:為什麼 inx0728 正常、我匯出的不正常?
-
欄位名差異:inx0728 使用原始英文鍵,匯出檔是中文展示鍵;若你的匯出流程用中文鍵去找值,會拿不到。
-
排序/分組鍵:inx0728 的排序與分組很單純;你的頁面因為篩選與合併(尤其跨欄位群組),容易誤把不同筆當相同而合併。
-
篩選殘留:inx0728 是原始全集,你的頁面可能保留了 Pinia 全域篩選或搜尋字串。
七、常見陷阱與避免法
-
陷阱:先清空 Pinia filter,再跑初始化 → 變成所有欄位沒值。
避免:先建好uniqueValues與filters,最後才回存 Pinia。 -
陷阱:用
Object.keys(映射表)當tableHeaders。
避免:用資料實際鍵名決定 header,再用映射表只負責顯示文字。 -
陷阱:合併前未排序或未分組。
避免:先依分組鍵排序,再逐段計算 rowSpan/merges。
八、最小可行範例(先確定筆數正確)
import * as XLSX from 'xlsx'
// 1) 不吃篩選,直接匯出完整資料
const dataToExport = mappedTableData.value
// 2) 不做任何合併與格式,只驗證筆數與欄位
const ws = XLSX.utils.json_to_sheet(dataToExport)
// 3) 可選:指定 header 順序,對齊參考檔
const headerOrder = ['APS_VERSION','APS_PLAN_NO','SEQ_NO','SITE','APPL','CUSTOMER_NAME','PRODUCT_ID']
const ws2 = XLSX.utils.json_to_sheet(
dataToExport.map(row => {
const o = {}
headerOrder.forEach(k => o[k] = row[k])
return o
}),
{ header: headerOrder }
)
// 4) 生成活頁簿
const wb = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(wb, ws2, '考題結論')
// 5) 後面再一段段加回格式/合併
九、上線前檢查清單(Checklist)
-
匯出來源是
mappedTableData(想完整 → YES;只匯篩選結果 →filteredData) -
初始化順序:先建
uniqueValues/filters,後存 Pinia -
tableHeaders取自資料鍵名,映射只做顯示 -
合併前先排序、正確分組;合併鍵名對齊資料鍵
-
無合併版匯出筆數與 inx0728 完全一致
-
逐步加回格式與合併,每一步都重新驗證筆數
FAQ
Q:為什麼畫面筆數跟匯出筆數不同?
A:因為你用
filteredData 匯出,或 Pinia
有殘留篩選。請先印出兩個長度確認。
Q:為什麼看起來有重複?
A:多半是合併(rowSpans/merges)把不同列壓在一起。先停用合併,驗證筆數後再逐步加回。
Q:欄位對不上 inx0728?
A:inx0728
用英文鍵名,匯出檔是中文標題。請用資料鍵名建表頭,最後再映射成中文顯示。
結語
匯出 Excel 筆數異常,99% 都是流程的小細節出錯——篩選來源、初始化順序、表頭映射、合併條件。
用「先純匯出 → 再加格式 → 最後合併」這個節奏,每一步驗證筆數,你就能快速鎖定問題、穩定上線。
留言
張貼留言