前端實現(xiàn)(excel)xlsx文件預(yù)覽的詳細步驟
1. 概述
接到一個任務(wù),是要前端實現(xiàn)文件預(yù)覽效果,百度了一圈,發(fā)現(xiàn)也沒有什么好的方法可以完美的將表格渲染出來。在前端中有sheetjs
和exceljs
可以對xlsx文件進行解析,本來一開始我用的是sheetjs
,但是在樣式獲取上遇到了麻煩,所以我改用了exceljs
,不過很難受,在樣式獲取時同樣遇到了不小的麻煩,但是我懶得換回sheetjs了,那就直接使用exceljs
吧。
要實現(xiàn)xlsx文件預(yù)覽效果,我的想法是使用一個庫對xlsx文件進行解析,然后使用另一個庫對解析出來的數(shù)據(jù)在頁面上進行繪制,綜上,我采用的方案是:exceljs+handsontable
2. 實現(xiàn)步驟
2.1 安裝庫
使用命令: npm i exceljs handsontable @handsontable/react
2.2 使用exceljs解析數(shù)據(jù)并使用handsontable進行渲染
直接貼代碼了:
import Excel from 'exceljs' import { useState } from 'react'; import { HotTable } from '@handsontable/react'; import { registerAllModules } from 'handsontable/registry'; import 'handsontable/dist/handsontable.full.min.css'; import { textRenderer, registerRenderer } from 'handsontable/renderers'; // 注冊模塊 registerAllModules(); export default function XLSXPreView() { const [data, setData] = useState([]); const handleFile = async (e) => { const file = e.target.files[0]; const workbook = new Excel.Workbook(); await workbook.xlsx.load(file) // 第一個工作表 const worksheet = workbook.getWorksheet(1); // 遍歷工作表中的所有行(包括空行) const sheetData = []; worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) { // console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values)); // 使用row.values獲取每一行的值時總會多出一條空數(shù)據(jù)(第一條),這里我把它刪除 const row_values = row.values.slice(1); sheetData.push(row_values) }); setData(sheetData); } return ( <> <input type="file" onChange={handleFile}/> <div id='table_view'> <HotTable data={data} readOnly={true} rowHeaders={true} colHeaders={true} width="100vw" height="auto" licenseKey='non-commercial-and-evaluation'// 一定得加這個,handsontable是收費的,加了這個才能免費用 /> </div> </> ) }
到這里,已經(jīng)實現(xiàn)了從xlsx文件中獲取數(shù)據(jù),并使用handsontable將表格中的數(shù)據(jù)渲染出來,示例結(jié)果如下,如果只需要將數(shù)據(jù)顯示出來,并不需要將樣式什么的一起復(fù)現(xiàn)了,那到這里就已經(jīng)結(jié)束了!
但事實上,這并不是我要做到效果,我的xlsx里面還有樣式什么的,也需要復(fù)現(xiàn),頭疼??
3. 其它的雜七雜八
3.1 單元格樣式
事實上,在exceljs解析xlsx文件時,它順帶一起把樣式獲取到了,通過worksheet.getCell(1, 1).style
可以獲取對應(yīng)單元格的樣式,如下,背景色存放在fill.fgColor
中,字體顏色存放在font.color
中,這樣的話只需要將這些樣式一一賦值給handsontable組件再添加樣式就好了。
但是實際操作的時候卻遇到了問題,先說excel中的顏色,在選擇顏色時,應(yīng)該都會打開下面這個選項框吧,如果你選擇的是標準色,它獲取到的顏色就是十六進制,但是如果你選擇主題中的顏色,那就是另一種結(jié)果了,并且還會有不同的深暗程度tint,這就很難受了!
隨后在控制臺中打印了workbook
,發(fā)現(xiàn)它把主題返回了,可以通過work._themes.theme1
獲取,不過獲取到的是xml格式的字符串,由于xml我沒學(xué),我不會,所以我就把它轉(zhuǎn)換成json來進行處理了。
第一步
安裝xml轉(zhuǎn)json的庫: npm i fast-xml-parser
import {XMLParser} from 'fast-xml-parser' // 將主題xml轉(zhuǎn)換成json const themeXml = workbook._themes.theme1; const options = { ignoreAttributes: false, attributeNamePrefix: '_' } const parser = new XMLParser(options); const json = parser.parse(themeXml) setThemeJson(json);
其實它的theme好像是固定的,也可以在一些格式轉(zhuǎn)換的網(wǎng)站中直接轉(zhuǎn)換成json然后放到一個json文件中,讀取就行,我這里就直接放到一個state中了!
第二步
接下來就是重頭戲了!設(shè)置單元格樣式…
首先安裝一個處理顏色的庫color,用來根據(jù)tint獲得不同明暗程度的顏色: npm i color
下面是獲取顏色的函數(shù):
// 根據(jù)主題和明暗度獲取顏色 const getThemeColor = (themeJson, themeId, tint) => { let color = ''; const themeColorScheme = themeJson['a:theme']['a:themeElements']['a:clrScheme']; switch (themeId) { case 0: color = themeColorScheme['a:lt1']['a:sysClr']['_lastClr']; break; case 1: color = themeColorScheme['a:dk1']['a:sysClr']['_lastClr']; break; case 2: color = themeColorScheme['a:lt2']['a:srgbClr']['_val']; break; case 3: color = themeColorScheme['a:dk2']['a:srgbClr']['_val']; break; default: color = themeColorScheme[`a:accent${themeId-3}`]['a:srgbClr']['_val']; break; } // 根據(jù)tint修改顏色深淺 color = '#' + color; const colorObj = Color(color); if(tint){ if(tint>0){// 淡色 color = colorObj.lighten(tint).hex(); }else{ // 深色 color = colorObj.darken(Math.abs(tint)).hex(); } } return color; } // 獲取顏色 const getColor = (obj, themeJson) => { if('argb' in obj){ // 標準色 // rgba格式去掉前兩位: FFFF0000 -> FF0000 return '#' + obj.argb.substring(2); }else if('theme' in obj){ // 主題顏色 if('tint' in obj){ return getThemeColor(themeJson, obj.theme, obj.tint); }else{ return getThemeColor(themeJson, obj.theme, null); } } }
然后設(shè)置handonsontable的單元格的一些樣式:顏色、加粗、下劃線、邊框balabala…的
順帶把行高和列寬一起設(shè)置了,這個還比較簡單,就一筆帶過了…
3.2 合并單元格
從獲取到的sheet中有一個_meages
屬性,該屬性中存放了表格中所有的合并單元格區(qū)域,所以只需要將它們重新渲染在handsontable中就好。
然后就實現(xiàn)了表格的一些基本功能的預(yù)覽,結(jié)果如下圖:
3. 總結(jié)(附全代碼)
其實這個的本質(zhì)主要就是通過ecxeljs解析表格文件的數(shù)據(jù),然后通過handsontable將它們重新繪制在頁面上,個人覺得這種方法并不好,因為表格里的操作太多了要把它們一一繪制工作量實在是太大了,而且很麻煩,我這里把表格的一些常用到的功能實現(xiàn)了預(yù)覽,還有想表格里放圖片什么的都沒有實現(xiàn),如果有需要,可以根據(jù)需求再進行進行寫。
我寫的其實還有一點bug,單元格的邊框樣式我只設(shè)置了solid和dashed,但事實上excel中單元格的邊框有12種樣式,而且還有對角線邊框,設(shè)置起來好麻煩,我就不弄了,大家用的時候注意一下哈,有需要的話可以自己修改一下!
附上全部代碼:
/** * exceljs + handsontable */ import Excel from 'exceljs' import { useState } from 'react'; import { HotTable } from '@handsontable/react'; import { registerAllModules } from 'handsontable/registry'; import 'handsontable/dist/handsontable.full.min.css'; import { textRenderer, registerRenderer } from 'handsontable/renderers'; import {XMLParser} from 'fast-xml-parser' import Color from 'color'; // 注冊模塊 registerAllModules(); // 根據(jù)主題和明暗度獲取顏色 const getThemeColor = (themeJson, themeId, tint) => { let color = ''; const themeColorScheme = themeJson['a:theme']['a:themeElements']['a:clrScheme']; switch (themeId) { case 0: color = themeColorScheme['a:lt1']['a:sysClr']['_lastClr']; break; case 1: color = themeColorScheme['a:dk1']['a:sysClr']['_lastClr']; break; case 2: color = themeColorScheme['a:lt2']['a:srgbClr']['_val']; break; case 3: color = themeColorScheme['a:dk2']['a:srgbClr']['_val']; break; default: color = themeColorScheme[`a:accent${themeId-3}`]['a:srgbClr']['_val']; break; } // 根據(jù)tint修改顏色深淺 color = '#' + color; const colorObj = Color(color); if(tint){ if(tint>0){// 淡色 color = colorObj.lighten(tint).hex(); }else{ // 深色 color = colorObj.darken(Math.abs(tint)).hex(); } } return color; } // 獲取顏色 const getColor = (obj, themeJson) => { if('argb' in obj){ // 標準色 // rgba格式去掉前兩位: FFFF0000 -> FF0000 return '#' + obj.argb.substring(2); }else if('theme' in obj){ // 主題顏色 if('tint' in obj){ return getThemeColor(themeJson, obj.theme, obj.tint); }else{ return getThemeColor(themeJson, obj.theme, null); } } } // 設(shè)置邊框 const setBorder = (style) =>{ let borderStyle = 'solid'; let borderWidth = '1px'; switch (style) { case 'thin': borderWidth = 'thin'; break; case 'dotted': borderStyle = 'dotted'; break; case 'dashDot': borderStyle = 'dashed'; break; case 'hair': borderStyle = 'solid'; break; case 'dashDotDot': borderStyle = 'dashed'; break; case 'slantDashDot': borderStyle = 'dashed'; break; case 'medium': borderWidth = '2px'; break; case 'mediumDashed': borderStyle = 'dashed'; borderWidth = '2px'; break; case 'mediumDashDotDot': borderStyle = 'dashed'; borderWidth = '2px'; break; case 'mdeiumDashDot': borderStyle = 'dashed'; borderWidth = '2px'; break; case 'double': borderStyle = 'double'; break; case 'thick': borderWidth = '3px'; break; default: break; } // console.log(borderStyle, borderWidth); return [borderStyle, borderWidth]; } export default function XLSXPreView() { // 表格數(shù)據(jù) const [data, setData] = useState([]); // 表格 const [sheet, setSheet] = useState([]); // 主題 const [themeJson, setThemeJson] = useState([]); // 合并的單元格 const [mergeRanges, setMergeRanges] = useState([]); registerRenderer('customStylesRenderer', (hotInstance, td, row, column, prop, value, cellProperties) => { textRenderer(hotInstance, td, row, column, prop, value, cellProperties); // console.log(cellProperties); // 填充樣式 if('fill' in cellProperties){ // 背景顏色 if('fgColor' in cellProperties.fill && cellProperties.fill.fgColor){ td.style.background = getColor(cellProperties.fill.fgColor, themeJson); } } // 字體樣式 if('font' in cellProperties){ // 加粗 if('bold' in cellProperties.font && cellProperties.font.bold){ td.style.fontWeight = '700'; } // 字體顏色 if('color' in cellProperties.font && cellProperties.font.color){ td.style.color = getColor(cellProperties.font.color, themeJson); } // 字體大小 if('size' in cellProperties.font && cellProperties.font.size){ td.style.fontSize = cellProperties.font.size + 'px'; } // 字體類型 if('name' in cellProperties.font && cellProperties.font.name){ td.style.fontFamily = cellProperties.font.name; } // 字體傾斜 if('italic' in cellProperties.font && cellProperties.font.italic){ td.style.fontStyle = 'italic'; } // 下劃線 if('underline' in cellProperties.font && cellProperties.font.underline){ // 其實還有雙下劃線,但是雙下劃綫css中沒有提供直接的設(shè)置方式,需要使用額外的css設(shè)置,所以我也就先懶得弄了 td.style.textDecoration = 'underline'; // 刪除線 if('strike' in cellProperties.font && cellProperties.font.strike){ td.style.textDecoration = 'underline line-through'; } }else{ // 刪除線 if('strike' in cellProperties.font && cellProperties.font.strike){ td.style.textDecoration = 'line-through'; } } } // 對齊 if('alignment' in cellProperties){ if('horizontal' in cellProperties.alignment){ // 水平 // 這里我直接用handsontable內(nèi)置類做了,設(shè)置成類似htLeft的樣子。 //(handsontable)其實至支持htLeft, htCenter, htRight, htJustify四種,但是其是它還有centerContinuous、distributed、fill,遇到這幾種就會沒有效果,也可以自己設(shè)置,但是我還是懶的弄了,用到的時候再說吧 const name = cellProperties.alignment.horizontal.charAt(0).toUpperCase() + cellProperties.alignment.horizontal.slice(1); td.classList.add(`ht${name}`); } if('vertical' in cellProperties.alignment){ // 垂直 // 這里我直接用handsontable內(nèi)置類做了,設(shè)置成類似htTop的樣子。 const name = cellProperties.alignment.vertical.charAt(0).toUpperCase() + cellProperties.alignment.vertical.slice(1); td.classList.add(`ht${name}`); } } // 邊框 if('border' in cellProperties){ if('left' in cellProperties.border && cellProperties.border.left){// 左邊框 const [borderWidth, borderStyle] = setBorder(cellProperties.border.left.style); let color = ''; // console.log(row, column, borderWidth, borderStyle); if(cellProperties.border.left.color){ color = getColor(cellProperties.border.left.color, themeJson); } td.style.borderLeft = `${borderStyle} ${borderWidth} ${color}`; } if('right' in cellProperties.border && cellProperties.border.right){// 左邊框 const [borderWidth, borderStyle] = setBorder(cellProperties.border.right.style); // console.log(row, column, borderWidth, borderStyle); let color = ''; if(cellProperties.border.right.color){ color = getColor(cellProperties.border.right.color, themeJson); } td.style.borderRight = `${borderStyle} ${borderWidth} ${color}`; } if('top' in cellProperties.border && cellProperties.border.top){// 左邊框 const [borderWidth, borderStyle] = setBorder(cellProperties.border.top.style); let color = ''; // console.log(row, column, borderWidth, borderStyle); if(cellProperties.border.top.color){ color = getColor(cellProperties.border.top.color, themeJson); } td.style.borderTop = `${borderStyle} ${borderWidth} ${color}`; } if('bottom' in cellProperties.border && cellProperties.border.bottom){// 左邊框 const [borderWidth, borderStyle] = setBorder(cellProperties.border.bottom.style); let color = ''; // console.log(row, column, borderWidth, borderStyle); if(cellProperties.border.bottom.color){ color = getColor(cellProperties.border.bottom.color, themeJson); } td.style.borderBottom = `${borderStyle} ${borderWidth} ${color}`; } } }); const handleFile = async (e) => { const file = e.target.files[0]; const workbook = new Excel.Workbook(); await workbook.xlsx.load(file) const worksheet = workbook.getWorksheet(1); // const sheetRows = worksheet.getRows(1, worksheet.rowCount); setSheet(worksheet) // console.log(worksheet.getCell(1, 1).style); // 遍歷工作表中的所有行(包括空行) const sheetData = []; worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) { // console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values)); // 使用row.values獲取每一行的值時總會多出一條空數(shù)據(jù)(第一條),這里我把它刪除 const row_values = row.values.slice(1); sheetData.push(row_values) }); setData(sheetData); // 將主題xml轉(zhuǎn)換成json const themeXml = workbook._themes.theme1; const options = { ignoreAttributes: false, attributeNamePrefix: '_' } const parser = new XMLParser(options); const json = parser.parse(themeXml) setThemeJson(json); // 獲取合并的單元格 const mergeCells = []; for(let i in worksheet._merges){ const {top, left, bottom, right} = worksheet._merges[i].model; mergeCells.push({ row: top-1, col: left-1, rowspan: bottom-top+1 , colspan: right-left+1}) } setMergeRanges(mergeCells) console.log(worksheet); } return ( <> <input type="file" onChange={handleFile}/> <div id='table_view'> <HotTable data={data} readOnly={true} rowHeaders={true} colHeaders={true} width="100vw" height="auto" licenseKey='non-commercial-and-evaluation' rowHeights={function(index) { if(sheet.getRow(index+1).height){ // exceljs獲取的行高不是像素值,事實上,它是23px - 13.8 的一個映射。所以需要將它轉(zhuǎn)化為像素值 return sheet.getRow(index+1).height * (23 / 13.8); } return 23;// 默認 }} colWidths={function(index){ if(sheet.getColumn(index+1).width){ // exceljs獲取的列寬不是像素值,事實上,它是81px - 8.22 的一個映射。所以需要將它轉(zhuǎn)化為像素值 return sheet.getColumn(index+1).width * (81 / 8.22); } return 81;// 默認 }} cells={(row, col, prop) => { const cellProperties = {}; const cellStyle = sheet.getCell(row+1, col+1).style if(JSON.stringify(cellStyle) !== '{}'){ // console.log(row+1, col+1, cellStyle); for(let key in cellStyle){ cellProperties[key] = cellStyle[key]; } } return {...cellProperties, renderer: 'customStylesRenderer'}; }} mergeCells={mergeRanges} /> </div> </> ) }
總結(jié)
到此這篇關(guān)于前端實現(xiàn)(excel)xlsx文件預(yù)覽的文章就介紹到這了,更多相關(guān)前端實現(xiàn)excel文件預(yù)覽內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用location.hash實現(xiàn)跨域iframe自適應(yīng)
其他一些類似js跨域操作問題也可以按這個思路去解決,需要的朋友可以測試下。2010-05-05JS實現(xiàn)Excel導(dǎo)出功能以及導(dǎo)出亂碼問題解決詳解
這篇文章主要為大家詳細介紹了JavaScript如何調(diào)用后臺接口實現(xiàn)Excel導(dǎo)出功能以及導(dǎo)出亂碼問題的解決辦法,需要的小伙伴可以參考一下2023-07-07JavaScript獲取flash對象與網(wǎng)上的有所不同
關(guān)于js獲取flash對象,網(wǎng)上有非常多的例子,但不是我想要的,經(jīng)測試整理,因此本文誕生了2014-04-04javascript框架設(shè)計之瀏覽器的嗅探和特征偵測
這篇文章主要介紹了javascript框架設(shè)計之瀏覽器的嗅探和特征偵測的相關(guān)資料,需要的朋友可以參考下2015-06-06JS前端框架關(guān)于重構(gòu)的失敗經(jīng)驗分享
關(guān)于重構(gòu)JS前端框架的失敗經(jīng)驗接下來與大家分享一下,感興趣的你可不要錯過了哈,畢竟是經(jīng)驗之談哈2013-03-03