這里給大家分享我在網(wǎng)上總結(jié)出來的一些知識,希望對大家有所幫助
前言
xlsx導(dǎo)出是比較前后端開發(fā)過程中都比較常見的一個功能。但傳統(tǒng)的二維表格可能很難能滿足我們對業(yè)務(wù)的需求,因為當(dāng)數(shù)據(jù)的維度和層次比較多時,二維表格很難以清晰和壓縮的方式展現(xiàn)所有的信息,所以我們也就經(jīng)常能碰到多級表頭開發(fā)了。
(相關(guān)資料圖)
demo
每當(dāng)我們新使用一個插件的時候,我們都可以看著官方文檔去新建立一個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已經(jīng)成功了,xlsx已經(jīng)下載下來了。
需求分析
- 新建一個表格
- 根據(jù)表頭將表格進(jìn)行合并
- 對合并后的表頭進(jìn)行內(nèi)容填充
- 填入數(shù)據(jù)內(nèi)容
效果如上圖(時間原因就先不寫xlsx的樣式了)。
需求實現(xiàn)
- 合并單元格: 需要指定開始的行和列以及結(jié)束的行和列,如
{ "s": { "r": 0, "c": 0 }, "e": { "r": 3, "c": 0 } }
,計算好需要合并的單元格后統(tǒng)一賦值給!merges
屬性。 - 合并單元格后填充內(nèi)容:由多個合并后的單元格填入內(nèi)容時,應(yīng)該也按照多個單元格填入,只是第一個有內(nèi)容,其他按空填入即可。
- 表頭結(jié)束后我們可以指定在某一行繼續(xù)填入內(nèi)容,即可繼續(xù)填入數(shù)據(jù)內(nèi)容。
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 } } ] // 合并單元格內(nèi)容 const wb = utils.book_new() utils.book_append_sheet(wb, ws, "Data") utils.sheet_add_aoa(ws, [ ["序號", "姓名", "性別", "公司概況", "", "", "", "", "", "備注"], ["", "", "", "職位", "項目", "", "", "", "公司名稱"], ["", "", "", "", "項目時長", "項目描述", "金額", ""], ["", "", "", "", "", "", "總金額", "利潤"] ], { origin: "A1" }) // 表頭內(nèi)容 utils.sheet_add_aoa(ws, [ [0, "張三", "男", "區(qū)域經(jīng)理", "3天", "暫無描述", 998, 9.98, "阿里巴巴", "暫無"], [1, "李四", "女", "CEO", "30天", "穩(wěn)了", 998, 9.98, "中石油", "暫無"] ], { origin: "A5" }) // 數(shù)據(jù)內(nèi)容 writeFileXLSX(wb, `${+new Date()}.xlsx`)}
好的,大功告成,今天就先到這里?
這東西也太丑了吧,我是一個開發(fā),我不是來這里數(shù)格子的??纯瓷厦娴拇a,我都不好意思說是我自己寫的。要不到同事電腦上提交一下吧?
數(shù)據(jù)分析
[ { "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 } }]
我想要轉(zhuǎn)成上面的數(shù)據(jù)結(jié)構(gòu),r從0開始,最大值就是它的深度,c從0開始,最大值就是它的廣度。因為這是一個多級表頭,每一級都會出現(xiàn)比上一級相等或更多子級的情況,我好像已經(jīng)把答案說到嘴邊了。對,就是用樹形結(jié)構(gòu)將其轉(zhuǎn)換處理。
我們結(jié)合上面已轉(zhuǎn)換好的列表結(jié)構(gòu)和下面準(zhǔn)備轉(zhuǎn)換的樹形結(jié)構(gòu),比如現(xiàn)在要合并第一個單元格序號
,我們應(yīng)該先找到起始位置,也就是0,0
,這個很好確定;我們單單從當(dāng)前節(jié)點并不能判斷真正的結(jié)束位置,我們應(yīng)該找到同級節(jié)點的最大深度,也就是公司概況
->項目
->金額
->總金額
,深度為3。所以它的結(jié)束位置應(yīng)該為3,0
。
當(dāng)我們要合并橫向單元格的時候,比如公司概況
,它下邊有三個子節(jié)點分別是職位
,項目
,公司名稱
,而子節(jié)點下方仍有不同的子節(jié)點,此時我們就應(yīng)該去獲取它們的每個子節(jié)點的每層子節(jié)點的總長度 - 1
,為什么要 - 1,因為當(dāng)前節(jié)點和第一個子節(jié)點占用的是同一個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" } ]
思路分析
- 找到當(dāng)前節(jié)點的深度和廣度
- 根據(jù)當(dāng)前節(jié)點深度和廣度,生成當(dāng)前節(jié)點單元格開始與結(jié)束位置
- 根據(jù)當(dāng)前節(jié)點深度和廣度,生成表頭數(shù)據(jù)結(jié)構(gòu)
- 根據(jù)最大深度位置,生成表單列表數(shù)據(jù)
代碼實現(xiàn)
tips: 如果你對樹結(jié)構(gòu)的遍歷還不太熟悉,可以看看【前端不求人】樹形結(jié)構(gòu)和一維數(shù)組,一笑泯恩仇
獲取當(dāng)前節(jié)點最大廣度和最大深度
- 遞歸發(fā)現(xiàn)當(dāng)前已無子節(jié)點時,就返回0,然后每返回一層就遞增1,每次返回時都獲取當(dāng)前節(jié)點的最大值,這樣就能獲得最深層數(shù)。
- 遞歸記錄每層每個子節(jié)點的長度 - 1,這樣就能獲取當(dāng)前列表的最大寬度。
- 我們使用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] }}
合并單元格開始和結(jié)束位置
- 獲取當(dāng)前節(jié)點的開始和結(jié)束位置
- 當(dāng)前節(jié)點無子節(jié)點,單元格寬為1,高為整個根節(jié)點的最大深度
- 當(dāng)前節(jié)點有子節(jié)點,單元格高為1,寬為當(dāng)前節(jié)點的寬,即最大廣度
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的空數(shù)組來記錄表頭內(nèi)容
- headerValue應(yīng)該是一個二維數(shù)組,headerValue[i][j]代表第i行第j列的內(nèi)容
- 當(dāng)發(fā)現(xiàn)當(dāng)前節(jié)點有children,直接獲取當(dāng)前節(jié)點的寬度,該寬度就是合并后空白單元格的個數(shù)。
- 當(dāng)發(fā)現(xiàn)當(dāng)前節(jié)點并沒有headerValue,表示前面的節(jié)點被縱向合并了,因此應(yīng)該直接加上這些空白單元格的節(jié)點
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
- 繼續(xù)遞歸mergedCells
- 收集無葉子節(jié)點的prop值
- 將prop值依次放進(jìn)一個數(shù)組中以備后續(xù)使用
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。
總結(jié)
我們通過計算當(dāng)前樹節(jié)點的大小,就可以獲取該節(jié)點的廣度和深度,通過廣度和深度又可以讓我們進(jìn)一步去演算當(dāng)前節(jié)點是否需要去合并其他單元格,是否需要生成空白單元格的數(shù)據(jù)內(nèi)容。生成表格內(nèi)容則只需要將最子層節(jié)點的prop收集,然后對應(yīng)取值即可。
本文轉(zhuǎn)載于:
https://juejin.cn/post/7243435843145678907
如果對您有所幫助,歡迎您點個關(guān)注,我會定時更新技術(shù)文檔,大家一起討論學(xué)習(xí),一起進(jìn)步。
關(guān)鍵詞: