這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 xlsx導出是比較前後端開發過程中都比較常見的一個功能。但傳統的二維表格可能很難能滿足我們對業務的需求,因為當數據的維度和層次比較多時,二維表格很難以清晰和壓縮的方式展現所有的信息,所以我們也就經常能碰到多級表頭開發了。 demo ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
前言
xlsx導出是比較前後端開發過程中都比較常見的一個功能。但傳統的二維表格可能很難能滿足我們對業務的需求,因為當數據的維度和層次比較多時,二維表格很難以清晰和壓縮的方式展現所有的信息,所以我們也就經常能碰到多級表頭開發了。
demo
每當我們新使用一個插件的時候,我們都可以看著官方文檔去新建立一個demo,然後去嘗試一下效果,這有助於我們分析錯誤。
npm i xlsx -S
function exportFile() { const ws = utils.json_to_sheet([]) const wb = utils.book_new() utils.sheet_add_aoa(ws, [ [1, 2, 3, 4, 5, 6, 7, 8, 9], ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'] ], { origin: 'A1' }) utils.book_append_sheet(wb, ws, 'Data') writeFileXLSX(wb, 'SheetJSVueAoO.xlsx') } exportFile()
demo已經成功了,xlsx已經下載下來了。
需求分析
- 新建一個表格
- 根據表頭將表格進行合併
- 對合併後的表頭進行內容填充
- 填入數據內容
效果如上圖(時間原因就先不寫xlsx的樣式了)。
需求實現
- 合併單元格: 需要指定開始的行和列以及結束的行和列,如
{ 's': { 'r': 0, 'c': 0 }, 'e': { 'r': 3, 'c': 0 } }
,計算好需要合併的單元格後統一賦值給!merges
屬性。 - 合併單元格後填充內容:由多個合併後的單元格填入內容時,應該也按照多個單元格填入,只是第一個有內容,其他按空填入即可。
- 表頭結束後我們可以指定在某一行繼續填入內容,即可繼續填入數據內容。
function exportFile() { const ws = utils.json_to_sheet([]) ws['!merges'] = [ { 's': { 'r': 0, 'c': 0 }, 'e': { 'r': 3, 'c': 0 } }, { 's': { 'r': 0, 'c': 1 }, 'e': { 'r': 3, 'c': 1 } }, { 's': { 'r': 0, 'c': 2 }, 'e': { 'r': 3, 'c': 2 } }, { 's': { 'r': 0, 'c': 3 }, 'e': { 'r': 0, 'c': 8 } }, { 's': { 'r': 1, 'c': 3 }, 'e': { 'r': 3, 'c': 3 } }, { 's': { 'r': 1, 'c': 4 }, 'e': { 'r': 1, 'c': 7 } }, { 's': { 'r': 2, 'c': 4 }, 'e': { 'r': 3, 'c': 4 } }, { 's': { 'r': 2, 'c': 5 }, 'e': { 'r': 3, 'c': 5 } }, { 's': { 'r': 2, 'c': 6 }, 'e': { 'r': 2, 'c': 7 } }, { 's': { 'r': 1, 'c': 8 }, 'e': { 'r': 3, 'c': 8 } }, { 's': { 'r': 0, 'c': 9 }, 'e': { 'r': 3, 'c': 9 } } ] // 合併單元格內容 const wb = utils.book_new() utils.book_append_sheet(wb, ws, 'Data') utils.sheet_add_aoa(ws, [ ['序號', '姓名', '性別', '公司概況', '', '', '', '', '', '備註'], ['', '', '', '職位', '項目', '', '', '', '公司名稱'], ['', '', '', '', '項目時長', '項目描述', '金額', ''], ['', '', '', '', '', '', '總金額', '利潤'] ], { origin: 'A1' }) // 表頭內容 utils.sheet_add_aoa(ws, [ [0, '張三', '男', '區域經理', '3天', '暫無描述', 998, 9.98, '阿裡巴巴', '暫無'], [1, '李四', '女', 'CEO', '30天', '穩了', 998, 9.98, '中石油', '暫無'] ], { origin: 'A5' }) // 數據內容 writeFileXLSX(wb, `${+new Date()}.xlsx`) }好的,大功告成,今天就先到這裡?
這東西也太醜了吧,我是一個開發,我不是來這裡數格子的。看看上面的代碼,我都不好意思說是我自己寫的。要不到同事電腦上提交一下吧?
數據分析
[ { 's': { 'r': 0, 'c': 0 }, 'e': { 'r': 3, 'c': 0 } }, { 's': { 'r': 0, 'c': 1 }, 'e': { 'r': 3, 'c': 1 } }, { 's': { 'r': 0, 'c': 2 }, 'e': { 'r': 3, 'c': 2 } }, { 's': { 'r': 0, 'c': 3 }, 'e': { 'r': 0, 'c': 8 } }, { 's': { 'r': 1, 'c': 3 }, 'e': { 'r': 3, 'c': 3 } }, { 's': { 'r': 1, 'c': 4 }, 'e': { 'r': 1, 'c': 7 } }, { 's': { 'r': 2, 'c': 4 }, 'e': { 'r': 3, 'c': 4 } }, { 's': { 'r': 2, 'c': 5 }, 'e': { 'r': 3, 'c': 5 } }, { 's': { 'r': 2, 'c': 6 }, 'e': { 'r': 2, 'c': 7 } }, { 's': { 'r': 1, 'c': 8 }, 'e': { 'r': 3, 'c': 8 } }, { 's': { 'r': 0, 'c': 9 }, 'e': { 'r': 3, 'c': 9 } } ]
我想要轉成上面的數據結構,r從0開始,最大值就是它的深度,c從0開始,最大值就是它的廣度。因為這是一個多級表頭,每一級都會出現比上一級相等或更多子級的情況,我好像已經把答案說到嘴邊了。對,就是用樹形結構將其轉換處理。
我們結合上面已轉換好的列表結構和下麵準備轉換的樹形結構,比如現在要合併第一個單元格序號
,我們應該先找到起始位置,也就是0,0
,這個很好確定;我們單單從當前節點並不能判斷真正的結束位置,我們應該找到同級節點的最大深度,也就是公司概況
->項目
->金額
->總金額
,深度為3。所以它的結束位置應該為3,0
。
當我們要合併橫向單元格的時候,比如公司概況
,它下邊有三個子節點分別是職位
,項目
,公司名稱
,而子節點下方仍有不同的子節點,此時我們就應該去獲取它們的每個子節點的每層子節點的總長度 - 1
,為什麼要 - 1,因為當前節點和第一個子節點占用的是同一個col
,因此可以需要減一。也就是說,如果公司概況的起始點為0,3
,那麼它的終止位置由此可推:職位+項目+公司名稱-1+項目時長+項目描述+金額-1+總金額+利潤-1
= 5
。所以終點位置為0,3+5
=> 0,8
const mergedCells = [ { name: '序號', prop: 'id' }, { name: '姓名', prop: 'name' }, { name: '性別', prop: 'sex' }, { name: '公司概況', children: [ { name: '職位', prop: 'jobTitle' }, { name: '項目', children: [ { name: '項目時長', prop: 'projectTime' }, { name: '項目描述', prop: 'projectDesc' }, { name: '金額', children: [ { name: '總金額', prop: 'total' }, { name: '利潤', prop: 'profit' } ] } ] }, { name: '公司名稱', prop: 'companyName' } ] }, { name: '備註', prop: 'remark' } ]
思路分析
- 找到當前節點的深度和廣度
- 根據當前節點深度和廣度,生成當前節點單元格開始與結束位置
- 根據當前節點深度和廣度,生成表頭數據結構
- 根據最大深度位置,生成表單列表數據
代碼實現
tips: 如果你對樹結構的遍歷還不太熟悉,可以看看【前端不求人】樹形結構和一維數組,一笑泯恩仇
獲取當前節點最大廣度和最大深度
- 遞歸發現當前已無子節點時,就返回0,然後每返回一層就遞增1,每次返回時都獲取當前節點的最大值,這樣就能獲得最深層數。
- 遞歸記錄每層每個子節點的長度 - 1,這樣就能獲取當前列表的最大寬度。
- 我們使用map做記錄,下次獲取就不需要重新計算了。
const map = new Map() const getCellsSize = list => { if (map.has(list)) { return map.get(list) } if (list?.length) { let rows = -1, cols = list.length - 1 list.forEach(item => { if (item.children) { const size = getCellsSize(item.children) rows = Math.max(size[0], rows) cols += size[1] } }) map.set(list, [rows + 1, cols]) return [rows + 1, cols] } }
合併單元格開始和結束位置
- 獲取當前節點的開始和結束位置
- 當前節點無子節點,單元格寬為1,高為整個根節點的最大深度
- 當前節點有子節點,單元格高為1,寬為當前節點的寬,即最大廣度
const size = getCellsSize(headers) const headerMerge = [] const mergeHeadersCell = (headers, row, col) => { for (let i = 0, len = headers.length;i < len;i++) { const cell = headers[i] if (!cell.children?.length) { if (row === size[0]) { continue } headerMerge.push({ s: { r: row, c: col + i }, e: { r: size[0], c: col + i } }) } else { const size = map.get(cell.children) headerMerge.push({ s: { r: row, c: col + i }, e: { r: row, c: col + size[1] + i }}) mergeHeadersCell(cell.children, row + 1, col + i) col += size[1] } } }
多表頭值填充
- 我們聲明一個headerValue的空數組來記錄表頭內容
- headerValue應該是一個二維數組,headerValue[i][j]代表第i行第j列的內容
- 當發現當前節點有children,直接獲取當前節點的寬度,該寬度就是合併後空白單元格的個數。
- 當發現當前節點並沒有headerValue,表示前面的節點被縱向合併了,因此應該直接加上這些空白單元格的節點
const headerValue = [] const getHeadersValue = (headers, row, col) => { if (!headerValue[row]) { headerValue[row] = new Array(col).fill('') } for (let i = 0, len = headers.length; i < len; i++) { const cell = headers[i] headerValue[row].push(cell.name) if (cell.children?.length) { const len = getCellsSize(cell.children)[1] const emptyNameList = new Array(len).fill('') headerValue[row].push(...emptyNameList) getHeadersValue(cell.children, row + 1, col + i) } } }
獲取列表prop
- 繼續遞歸mergedCells
- 收集無葉子節點的prop值
- 將prop值依次放進一個數組中以備後續使用
const bodyMapList = [] const getBodyMapList = list => { if (list?.length) { list.forEach(item => { !item.children ? bodyMapList.push(item.prop) : getBodyMapList(item.children) }) } } list.map(item => bodyMapList.map(key => item[key]))
以上就是核心代碼展示啦,如果想看完整代碼,可以到github觀看,歡迎star。
總結
我們通過計算當前樹節點的大小,就可以獲取該節點的廣度和深度,通過廣度和深度又可以讓我們進一步去演算當前節點是否需要去合併其他單元格,是否需要生成空白單元格的數據內容。生成表格內容則只需要將最子層節點的prop收集,然後對應取值即可。