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