項(xiàng)目功能
- 任意層級(jí)合并單元格復(fù)雜表頭解析
- 自動(dòng)轉(zhuǎn)化為目標(biāo)層級(jí)的數(shù)據(jù)結(jié)構(gòu)
- 自動(dòng)生成基于 antdv 的 table 列配置數(shù)據(jù) columns 及對(duì)于數(shù)據(jù)源 dataSource。在頁(yè)面端復(fù)現(xiàn) Excel 效果。
在線示例
- 步驟零:如需快速測(cè)試,可點(diǎn)擊頂部的示例按鈕,可快速填充測(cè)試數(shù)據(jù),并自動(dòng)下載對(duì)應(yīng)的 Excel 文件,點(diǎn)擊上傳 Excel 文件即可復(fù)現(xiàn)整個(gè)使用流程
-
步驟一:輸入「Excel 表頭結(jié)構(gòu)字符串」與「目標(biāo)數(shù)據(jù)結(jié)構(gòu) key」之間的映射關(guān)系
- "key" 為 Excel 表頭,每一列的所處層級(jí)關(guān)系。如「基礎(chǔ)信息.年齡」對(duì)應(yīng)的就是 Excel 表在第二列中的關(guān)系,第一級(jí)是「基礎(chǔ)信息」,第二級(jí)是「年齡」
- "value" 為需要轉(zhuǎn)換的目標(biāo)數(shù)據(jù)結(jié)構(gòu)的層級(jí)關(guān)系。「baseInfo.age」的意思是,將 Excel 表第二列的數(shù)據(jù),轉(zhuǎn)化為目標(biāo)對(duì)象中.baseInfo.age 這個(gè)屬性。
- 【特別注意一】"key":"value" 的映射關(guān)系,沒(méi)有順序的要求,無(wú)需要按 Excel 表的每一列的數(shù)據(jù)進(jìn)行排序。
- 【特別注意二】Excel 表的層級(jí)映射關(guān)系與目標(biāo)對(duì)象的層級(jí)映射關(guān)系,沒(méi)有強(qiáng)制約束和要求。如Excel 中的 "手機(jī)號(hào)",只有一層結(jié)構(gòu),但完全可以轉(zhuǎn)化為目標(biāo)對(duì)象中的 "contact.phone" 二級(jí)結(jié)構(gòu)。反之亦然。
-
步驟二:點(diǎn)擊右側(cè),上傳對(duì)應(yīng)的 Excel 文件,即可完成 Excel 解析。
解析后的頁(yè)面介紹
-
頂部為基于 antdv 的 table 組件的復(fù)現(xiàn)效果。復(fù)現(xiàn) Excel 表格中,合并完單元格后的效果。 table 組件用到的 columns 配置,及 dataSource 數(shù)據(jù),均由解析函數(shù)一并返回。無(wú)需要開(kāi)發(fā)者二次開(kāi)發(fā)及維護(hù)。
-
底部為解析函數(shù)解析后返回的三個(gè)數(shù)據(jù)結(jié)果。分別為
- 解析后的 antdv columns 表格列的配置項(xiàng),直接傳遞給 table 組件的 columns 屬性使用。
- 解析后的 antdv dataSource 表格數(shù)據(jù)源,直接傳遞給 table 組件的 data-source 屬性使用。
- 解析后的目標(biāo)數(shù)據(jù)結(jié)構(gòu)數(shù)組。即根據(jù)步驟一設(shè)置的映射表,將 Excel 各個(gè)單元格數(shù)據(jù),轉(zhuǎn)換后的目標(biāo)數(shù)據(jù)結(jié)構(gòu)。一般情況下,該數(shù)據(jù)結(jié)構(gòu),即為傳遞給后端的數(shù)據(jù)結(jié)構(gòu)。
背景,解決什么問(wèn)題
- 直接使用 SheetJS 的 XLSX.utils.sheet_to_json 函數(shù)進(jìn)行 excel 數(shù)據(jù)轉(zhuǎn)化時(shí),僅支持一行表頭的 Excel 表格數(shù)據(jù)解析(只識(shí)別 Excel 內(nèi)容的第一行作為標(biāo)題),無(wú)法識(shí)別表頭進(jìn)行過(guò)合并單元格的 Excel 數(shù)據(jù)解析
- sheet_to_json 轉(zhuǎn)化的 JSON 數(shù)據(jù),是以中文為 key 的對(duì)象,不符合編程習(xí)慣,需要開(kāi)發(fā)者手動(dòng)進(jìn)行數(shù)據(jù)中英文 key 轉(zhuǎn)換,自行轉(zhuǎn)換為目標(biāo)數(shù)據(jù)結(jié)構(gòu)。
- excel 表頭的組織與復(fù)現(xiàn),進(jìn)行 Excel 表上傳時(shí),通常需要在前端展現(xiàn)表格內(nèi)容,給用戶需要數(shù)據(jù)復(fù)現(xiàn)及確認(rèn)。這個(gè)過(guò)程需要開(kāi)發(fā)者手動(dòng)組織完成。
- 最少知識(shí)原則,提供最小化 demo 示例,沒(méi)有腳手架,無(wú)需要安裝 npm 依賴包。純 html + js 文件??焖贉y(cè)試代碼可行性。邊改邊測(cè)進(jìn)行二次開(kāi)發(fā)。
具體函數(shù)使用方式
- 示例遵循最少知識(shí)原則,項(xiàng)目中的 core.js 文件,即為轉(zhuǎn)換函數(shù)所在文件。里面一共不到 200 行代碼??梢灾苯诱迟N復(fù)制到所需項(xiàng)目里面。
- 示例直接使用的 script 文件整體引入的方式。所以遷移到基于 npm 的項(xiàng)目中時(shí),需要將 core.js 中的 ‘XLSX.utils’ 全局變量使用方式,改為通過(guò)?
import { utils } from 'xlsx'
?的使用方式。
- 如果本項(xiàng)目確實(shí)有幫助到小伙伴,小伙伴有需求的話,可以在 github 中提 issues,有需要的話,將補(bǔ)充基于 npm 的版本,以及帶上 ts 類型約束的版本。
轉(zhuǎn)換函數(shù)運(yùn)行邏輯
-
獲取 Excel 所有單元格數(shù)據(jù)
- 借用 SheetJS 的 'encode_cell' 方法,及 'format_cell' 方法,遍歷獲取每個(gè) sheet 中,每個(gè)單元格的數(shù)據(jù)。
/** * 獲取所有單元格數(shù)據(jù) * @param sheet sheet 對(duì)象 * @returns 該 sheet 所有單元格數(shù)據(jù) */ function getSheetCells(sheet) { if (!sheet || !sheet['!ref']) { return [] } const range = XLSX.utils.decode_range(sheet['!ref']) let allCells = [] for (let rowIndex = range.s.r; rowIndex <= range.e.r; ++rowIndex) { let newRow = [] allCells.push(newRow) for (let colIndex = range.s.c; colIndex <= range.e.c; ++colIndex) { const cell = sheet[XLSX.utils.encode_cell({ c: colIndex, r: rowIndex })] let cellContent = '' if (cell && cell.t) { cellContent = XLSX.utils.format_cell(cell) } newRow.push(cellContent) } } return allCells }
-
根據(jù) map 映射表,獲取 Excel 表頭行數(shù)
- 遍歷 map 中的每個(gè)中文 key,根據(jù)'點(diǎn)'關(guān)鍵字拆分層級(jí),找到 Excel 最高層級(jí)。從而得到 Excel 表頭的行數(shù)。區(qū)分那些行是表頭,那些行是數(shù)據(jù)。
// 獲取菜單項(xiàng)在 Excel 中所占行數(shù) function getHeaderRowNum(textKeyMap) { let maxLevel = 1 // 最高層級(jí) Object.keys(textKeyMap).forEach(textStr => { maxLevel = Math.max(maxLevel, textStr.split('.').length) }) return maxLevel } const headerRowNum = getHeaderRowNum(textKeyMap)
-
通過(guò)容器,遞歸解析 Excel 表頭配置樹(shù)裝數(shù)據(jù)結(jié)構(gòu)
- 通過(guò) lastHeaderLevelColumns 遍歷,保存在 Excel 中的每一行的最新容器。
- 當(dāng)表頭遍歷到非空單元格時(shí),往上一級(jí)的容器中,加入本層級(jí)配置。
- 從而遞歸解析,得到 table 表頭層級(jí)結(jié)構(gòu)數(shù)組。
-
使用索引映射表,將 Excel 表頭對(duì)象格與 Excel 所在行索引進(jìn)行掛鉤
- 在遞歸解析表頭層級(jí)結(jié)構(gòu)時(shí),通過(guò) columnIndexObjMap,記錄當(dāng)前表頭數(shù)據(jù),所在列。
let headerColumns = [] // 收集 table 組件中,表頭 columns 的對(duì)象數(shù)組結(jié)構(gòu) const lastHeaderLevelColumns = [] // 最近一個(gè) columns,用于收集單元格子表頭的內(nèi)容 const textValueMaps = [] // 以中文字符串為 key 的對(duì)象數(shù)組,用于收集表格中的數(shù)據(jù) const columnIndexObjMap = [] // 表的列索引,對(duì)應(yīng)在對(duì)象中的位置,用于后續(xù)獲取表格數(shù)據(jù)時(shí),快速定位每一個(gè)單元格 for (let colIndex = 0; colIndex < headerRows[0].length; colIndex++) { const headerCellList = headerRows.map(item => item[colIndex]) headerCellList.forEach((headerCell, headerCellIndex) => { // 如果當(dāng)前單元格沒(méi)數(shù)據(jù),這證明是合并后的單元格,跳過(guò)其處理 if (!headerCell) { return } const tempColumn = { title: headerCell } columnIndexObjMap[colIndex] = tempColumn // 通過(guò) columnIndexObjMap 記錄每一列數(shù)據(jù),對(duì)應(yīng)到那個(gè)對(duì)象中,實(shí)現(xiàn)一個(gè)映射表 // 如果表頭數(shù)據(jù)第一輪就有值,這證明這是新起的一個(gè)表頭項(xiàng)目,往 headerColumns 中,新加入一條數(shù)據(jù) if (headerCellIndex === 0) { headerColumns.push(tempColumn) lastHeaderLevelColumns[headerCellIndex] = tempColumn // 記錄當(dāng)前層級(jí),最新的一個(gè)表格容器,可能在下一列數(shù)據(jù)時(shí)合并單元格,下一個(gè)層級(jí)需要往該容器中添加數(shù)據(jù) } else { // 不是第一列數(shù)據(jù),這證明是子項(xiàng)目,需要加入到上一層表頭的 children 項(xiàng),作為其子項(xiàng)目 lastHeaderLevelColumns[headerCellIndex - 1].children = lastHeaderLevelColumns[headerCellIndex - 1].children || [] lastHeaderLevelColumns[headerCellIndex - 1].children.push(tempColumn) lastHeaderLevelColumns[headerCellIndex] = tempColumn // 記錄當(dāng)前層級(jí)的容器索引,可能再下一層級(jí)中使用到 } }) }
-
利用該索引表,遍歷 Excel 數(shù)據(jù)表的每一行,快速生成每行 Excel 的數(shù)據(jù)結(jié)構(gòu)
- 利用 columnIndexObjMap,遍歷 Excel 數(shù)據(jù)表的每一行,往 headerColumns 配置中,插入 value 值,將其設(shè)置為特定行,對(duì)應(yīng)列的數(shù)據(jù)。
- 通過(guò) Object.create 從 headerColumns 中生成一個(gè)對(duì)象副本。
// 將以數(shù)組形式記錄的對(duì)象信息,轉(zhuǎn)化為正常的對(duì)象結(jié)構(gòu) function transformListToObj(listObjs) { const resultObj = {} listObjs.forEach(item => { if (item.value) { resultObj[item.title] = item.value return } if (item.children && item.children.length > 0) { resultObj[item.title] = transformListToObj(item.children) } }) return resultObj } // 以 headerColumns 為對(duì)象結(jié)構(gòu)模板,遍歷 excel 表數(shù)據(jù)中的所有數(shù)據(jù),并利用 columnIndexObjMap 的映射,快速填充每一項(xiàng)數(shù)據(jù) dataRows.forEach(dataRow => { dataRow.forEach((value, index) => { columnIndexObjMap[index].value = value }) const titleObj = Object.create(headerColumns) // columnIndexObjMap 的指針?biāo)饕瑑H指向 headerColumns,以 headerColumns 為數(shù)據(jù)模板,復(fù)制一份數(shù)據(jù),獲得數(shù)據(jù)填充后的效果對(duì)象 textValueMaps.push(transformListToObj(titleObj)) // 將 listObj 對(duì)象轉(zhuǎn)化化普通的對(duì)象 })
- 將 JSON 數(shù)據(jù)結(jié)構(gòu),進(jìn)行 key map 映射解析,生成目標(biāo)數(shù)據(jù)結(jié)構(gòu)
/** * 將以點(diǎn)拼接的扁平字符串對(duì)象,解析為具有深度的對(duì)象 * @param dotStrObj 點(diǎn)拼接的扁平化字符串對(duì)象 * @returns 具有深度的對(duì)象 */ function parseDotStrObjToObj(dotStrObj) { const resultObj = {} Object.keys(dotStrObj).forEach(key => { let keys = key.split('.') keys.reduce((resultObj, curValue, currentIndex) => { resultObj[curValue] = currentIndex === keys.length - 1 ? dotStrObj[key] : resultObj[curValue] || {} return resultObj[curValue] }, resultObj) }) return resultObj } /** * 將具有深度的對(duì)象扁平化,變成以點(diǎn)拼接的扁平字符串對(duì)象 * @param targetObj 具有深度的對(duì)象 * @returns 扁平化后,由點(diǎn)操作符拼接的對(duì)象 */ function transformObjToDotStrObj(targetObj) { const resultObj = {} function transform(currentObj, preKeys) { Object.keys(currentObj).forEach(key => { if (Object.prototype.toString.call(currentObj[key]) !== '[object Object]') { resultObj[[...preKeys, key].join('.')] = currentObj[key] } else { transform(currentObj[key], [...preKeys, key]) } }) } transform(targetObj, []) return resultObj } // 將以中文為 key 的對(duì)象,通過(guò) textKeyMap 映射,找到對(duì)應(yīng)的 key,轉(zhuǎn)化為以 key 對(duì)鍵的對(duì)象,轉(zhuǎn)化為后端對(duì)應(yīng)的 json 對(duì)象 function transformTextToKey(textDataList, textKeyMap) { const textDotStrDataList = textDataList.map(obj => transformObjToDotStrObj(obj)) let textDotStrDataListStr = JSON.stringify(textDotStrDataList) Object.keys(textKeyMap).forEach(text => { const key = textKeyMap[text] textDotStrDataListStr = textDotStrDataListStr.replaceAll(`"${text}"`, `"${key}"`) // 在這里,通過(guò)字符串的替換,實(shí)現(xiàn)表頭數(shù)據(jù)層級(jí)結(jié)構(gòu),與實(shí)際對(duì)象將數(shù)據(jù)結(jié)構(gòu)的轉(zhuǎn)換 }) const keyDotStrDataList = JSON.parse(textDotStrDataListStr) const keyDataList = keyDotStrDataList.map(keyDotStrData => parseDotStrObjToObj(keyDotStrData)) return keyDataList }
- 返回給 antdv 復(fù)現(xiàn) table 用的 columns 配置,dataSource 表格數(shù)據(jù),以及 dataList 后端 JSON 數(shù)據(jù)
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-792300.html
- 上述源碼確實(shí)不太好懂,不太好描述,如果本項(xiàng)目確實(shí)能幫助到小伙伴,而小伙伴對(duì)源碼也感興趣的話。可以提 issues,我再后補(bǔ)運(yùn)行邏輯詳解及配圖
全部核心源碼
/** * 將以點(diǎn)拼接的扁平字符串對(duì)象,解析為具有深度的對(duì)象 * @param dotStrObj 點(diǎn)拼接的扁平化字符串對(duì)象 * @returns 具有深度的對(duì)象 */ function parseDotStrObjToObj(dotStrObj) { const resultObj = {} Object.keys(dotStrObj).forEach(key => { let keys = key.split('.') keys.reduce((resultObj, curValue, currentIndex) => { resultObj[curValue] = currentIndex === keys.length - 1 ? dotStrObj[key] : resultObj[curValue] || {} return resultObj[curValue] }, resultObj) }) return resultObj } /** * 將具有深度的對(duì)象扁平化,變成以點(diǎn)拼接的扁平字符串對(duì)象 * @param targetObj 具有深度的對(duì)象 * @returns 扁平化后,由點(diǎn)操作符拼接的對(duì)象 */ function transformObjToDotStrObj(targetObj) { const resultObj = {} function transform(currentObj, preKeys) { Object.keys(currentObj).forEach(key => { if (Object.prototype.toString.call(currentObj[key]) !== '[object Object]') { resultObj[[...preKeys, key].join('.')] = currentObj[key] } else { transform(currentObj[key], [...preKeys, key]) } }) } transform(targetObj, []) return resultObj } /** * 獲取所有單元格數(shù)據(jù) * @param sheet sheet 對(duì)象 * @returns 該 sheet 所有單元格數(shù)據(jù) */ function getSheetCells(sheet) { if (!sheet || !sheet['!ref']) { return [] } const range = XLSX.utils.decode_range(sheet['!ref']) let allCells = [] for (let rowIndex = range.s.r; rowIndex <= range.e.r; ++rowIndex) { let newRow = [] allCells.push(newRow) for (let colIndex = range.s.c; colIndex <= range.e.c; ++colIndex) { const cell = sheet[XLSX.utils.encode_cell({ c: colIndex, r: rowIndex })] let cellContent = '' if (cell && cell.t) { cellContent = XLSX.utils.format_cell(cell) } newRow.push(cellContent) } } return allCells } /** * 獲取表頭任意層級(jí)單元格合并后的表格內(nèi)容解析 * @param sheet 一個(gè) sheet 中所有單元格內(nèi)容 * @param textKeyMap 表頭中文與對(duì)應(yīng)英文 key 之間的映射表 * @returns antdv 中的表格 column,dataSource,以及轉(zhuǎn)化后的,直接傳輸給后端的 json 對(duì)象數(shù)組 */ function getSheetHeaderAndData(sheet, textKeyMap) { // 獲取菜單項(xiàng)在 Excel 中所占行數(shù) function getHeaderRowNum(textKeyMap) { let maxLevel = 1 // 最高層級(jí) Object.keys(textKeyMap).forEach(textStr => { maxLevel = Math.max(maxLevel, textStr.split('.').length) }) return maxLevel } const headerRowNum = getHeaderRowNum(textKeyMap) const headerRows = sheet.slice(0, headerRowNum) const dataRows = sheet.slice(headerRowNum) let headerColumns = [] // 收集 table 組件中,表頭 columns 的對(duì)象數(shù)組結(jié)構(gòu) const lastHeaderLevelColumns = [] // 最近一個(gè) columns,用于收集單元格子表頭的內(nèi)容 const textValueMaps = [] // 以中文字符串為 key 的對(duì)象數(shù)組,用于收集表格中的數(shù)據(jù) const columnIndexObjMap = [] // 表的列索引,對(duì)應(yīng)在對(duì)象中的位置,用于后續(xù)獲取表格數(shù)據(jù)時(shí),快速定位每一個(gè)單元格 for (let colIndex = 0; colIndex < headerRows[0].length; colIndex++) { const headerCellList = headerRows.map(item => item[colIndex]) // eslint-disable-next-line no-loop-func headerCellList.forEach((headerCell, headerCellIndex) => { // 如果當(dāng)前單元格沒(méi)數(shù)據(jù),這證明是合并后的單元格,跳過(guò)其處理 if (!headerCell) { return } const tempColumn = { title: headerCell } columnIndexObjMap[colIndex] = tempColumn // 通過(guò) columnIndexObjMap 記錄每一列數(shù)據(jù),對(duì)應(yīng)到那個(gè)對(duì)象中,實(shí)現(xiàn)一個(gè)映射表 // 如果表頭數(shù)據(jù)第一輪就有值,這證明這是新起的一個(gè)表頭項(xiàng)目,往 headerColumns 中,新加入一條數(shù)據(jù) if (headerCellIndex === 0) { headerColumns.push(tempColumn) lastHeaderLevelColumns[headerCellIndex] = tempColumn // 記錄當(dāng)前層級(jí),最新的一個(gè)表格容器,可能在下一列數(shù)據(jù)時(shí)合并單元格,下一個(gè)層級(jí)需要往該容器中添加數(shù)據(jù) } else { // 不是第一列數(shù)據(jù),這證明是子項(xiàng)目,需要加入到上一層表頭的 children 項(xiàng),作為其子項(xiàng)目 lastHeaderLevelColumns[headerCellIndex - 1].children = lastHeaderLevelColumns[headerCellIndex - 1].children || [] lastHeaderLevelColumns[headerCellIndex - 1].children.push(tempColumn) lastHeaderLevelColumns[headerCellIndex] = tempColumn // 記錄當(dāng)前層級(jí)的容器索引,可能再下一層級(jí)中使用到 } }) } // 運(yùn)行以上代碼,得到 headerColumns,以及 headerColumns 中,每個(gè)對(duì)象對(duì)應(yīng)在表格中的哪一行索引 // 將以數(shù)組形式記錄的對(duì)象信息,轉(zhuǎn)化為正常的對(duì)象結(jié)構(gòu) function transformListToObj(listObjs) { const resultObj = {} listObjs.forEach(item => { if (item.value) { resultObj[item.title] = item.value return } if (item.children && item.children.length > 0) { resultObj[item.title] = transformListToObj(item.children) } }) return resultObj } // 以 headerColumns 為對(duì)象結(jié)構(gòu)模板,遍歷 excel 表數(shù)據(jù)中的所有數(shù)據(jù),并利用 columnIndexObjMap 的映射,快速填充每一項(xiàng)數(shù)據(jù) dataRows.forEach(dataRow => { dataRow.forEach((value, index) => { columnIndexObjMap[index].value = value }) const titleObj = Object.create(headerColumns) // columnIndexObjMap 的指針?biāo)饕?,僅指向 headerColumns,以 headerColumns 為數(shù)據(jù)模板,復(fù)制一份數(shù)據(jù),獲得數(shù)據(jù)填充后的效果對(duì)象 textValueMaps.push(transformListToObj(titleObj)) // 將 listObj 對(duì)象轉(zhuǎn)化化普通的對(duì)象 }) // 根據(jù)表頭的 title 值,從 textKeyMap 中尋找映射關(guān)系,設(shè)置 headerColumn 對(duì)應(yīng)的 dataIndex function setHeaderColumnDataIndex(headerColumns, preTitle) { headerColumns.forEach(headerObj => { if (headerObj.children) { headerObj.children = setHeaderColumnDataIndex(headerObj.children, [...preTitle, headerObj.title]) } else { const titleStr = [...preTitle, headerObj.title].join('.') headerObj.dataIndex = textKeyMap[titleStr] } }) return headerColumns } // 將以中文為 key 的對(duì)象,通過(guò) textKeyMap 映射,找到對(duì)應(yīng)的 key,轉(zhuǎn)化為以 key 對(duì)鍵的對(duì)象,轉(zhuǎn)化為后端對(duì)應(yīng)的 json 對(duì)象 function transformTextToKey(textDataList, textKeyMap) { const textDotStrDataList = textDataList.map(obj => transformObjToDotStrObj(obj)) let textDotStrDataListStr = JSON.stringify(textDotStrDataList) Object.keys(textKeyMap).forEach(text => { const key = textKeyMap[text] textDotStrDataListStr = textDotStrDataListStr.replaceAll(`"${text}"`, `"${key}"`) // 在這里,通過(guò)字符串的替換,實(shí)現(xiàn)表頭數(shù)據(jù)層級(jí)結(jié)構(gòu),與實(shí)際對(duì)象將數(shù)據(jù)結(jié)構(gòu)的轉(zhuǎn)換 }) const keyDotStrDataList = JSON.parse(textDotStrDataListStr) const keyDataList = keyDotStrDataList.map(keyDotStrData => parseDotStrObjToObj(keyDotStrData)) return keyDataList } headerColumns = setHeaderColumnDataIndex(headerColumns, []) const dataList = transformTextToKey(textValueMaps, textKeyMap) const dataSource = dataList.map(row => transformObjToDotStrObj(row)) // 將 JSON 對(duì)象轉(zhuǎn)化為 “點(diǎn).” 拼接的扁平對(duì)象,使得數(shù)據(jù)與 headerColumn 中的 dataIndex 相對(duì)應(yīng)。實(shí)現(xiàn) table 的數(shù)據(jù)填充 return { headerColumns, dataList, dataSourceList: dataSource, } }
這里還有 Excel 合并單元格復(fù)雜表頭導(dǎo)出解析示例
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-792300.html
到了這里,關(guān)于基于 SheetJS 的前端合并單元格復(fù)雜表頭導(dǎo)入的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!