vue3 前端解析带图片的excel
将带图片的excel上传后解析数据,然后将数据放入表格。主要是为了将数据传给服务端
目前存在的问题
1、最后得到的数据需要自己处理一下
2、目前最多只支持解析26列,如果超过26列,需要自己去rowKeys里面加数据 'AA','AB','AC'.....
需要注意
1、keys需要根据列数来设置,有几列就设置几个字段,如果少了可能会有问题
需要安装 xlsx , jszip
npm install xlsx jszip
<template> <div class="container"> <!-- 长传组件 --> <el-upload action="" :before-upload="beforeUpload" :http-request="() => { }"> <el-button type="primary">导入excel</el-button> </el-upload> <!-- 表格组件 --> <el-table :data="tableData" border style="width: auto; margin-top: 10px" :span-method="arraySpanMethod"> <el-table-column :prop="item.prop" :label="item.label" align="center" v-for="(item, index) in tableColumnLabel" :key="index"> <template #default="scope" v-if="item.prop == 'images'"> <!-- 这里把它打开的话,images里面的数据自己处理一下 --> <!-- <img :src="img.path" alt="" style="width: 200px" v-for="img in scope.row.images" /> --> </template> </el-table-column> </el-table> </div> </template> <script setup lang="ts"> import { onMounted, ref } from "vue"; import JSZip from "jszip"; // 引入jszip import type { JSZipObject } from 'jszip'; import * as XLSX from "xlsx"; // 引入xlsx const tableColumnLabel = ref(); // 获取表头内容 const tableData = ref<any[]>([]); // 表格数据 const tableImages = ref<imageList>([]); // 表格图片 const rowKeys = ref<string[]>([])//列数 A,B,C,D,E,F,G...X,Y,Z只支持上传26列,超过26,可能会出现未知问题 const tabCellList = ref();//合并的单元格数据['A2:A4','B2:B4','A6:A7','C6:C7'] const tabskeys = ref(['label', 'images', 'label1', 'label2', 'label3', 'label4']);//这里是标题的label,很重要,有几列,就搞几个 onMounted(() => { for (var i = 0; i < 26; i++) { //输出A-Z 26个大写字母 rowKeys.value.push(String.fromCharCode(65 + i)); }
//这里为了多增加几列,如果需要解析的数据超过26列,可以一直push AA,AB,AC,AD,AE..... rowKeys.value.push('AA'); }); /** * el-ui table 合并单元格 * @param param0 */ const arraySpanMethod = ({ rowIndex, columnIndex, }: any) => { let list = tabCellList.value as any[]; if (list.length == 0) return { rowspan: 1, colspan: 1 } let span = { rowspan: 1, colspan: 1 } list.forEach(item => { const [start, end] = item.split(':'); let s = countCell(start); let e = countCell(end); if (columnIndex === s.x && rowIndex === s.y - 2) { const rowspan = e.y - s.y + 1; const colspan = e.x - s.x + 1; span = { rowspan, colspan } } else if ((columnIndex >= s.x && columnIndex <= e.x) && (rowIndex >= s.y - 2 && rowIndex <= e.y - 2)) { span = { rowspan: 0, colspan: 0 } } }); return span } // 上传excel const beforeUpload = async (file: any) => { // 处理解析图片,获取合并的单元格数据(需要使用jszip) const { imageList, cellList } = await getExcelZip(file); tableImages.value = imageList; tabCellList.value = cellList; console.log('那些单元格合并了:', tabCellList.value); // 解析数据 getExcelData(file); } // 处理解析Zip async function getExcelZip(file: any) { let result: any[] = []; // 用来存放图片 let resultCell: any[] = [];//合并的单元格数据 const zip = new JSZip(); // 创建jszip实例 try { let zipLoadResult = await zip.loadAsync(file); // 将xlsx文件转zip文件 const zipLoadResultFiles = zipLoadResult["files"]; const imageList = await analysisImageList(zipLoadResultFiles); result = await analysisImageLocation(zipLoadResultFiles, imageList); resultCell = await getCelllist(zipLoadResultFiles); } catch (error) { console.log(error); } return { imageList: result, cellList: resultCell }; } // 解析获取合并的单元格数据 const getCelllist = async (zipLoadResultFiles: { [x: string]: JSZip.JSZipObject }) => { const imageLocationKey = 'xl/worksheets/sheet1.xml';//图片坐标文件路径 const fileContent = await zipLoadResultFiles[imageLocationKey].async('string'); let parser = new DOMParser(); let xmldom = parser.parseFromString(fileContent, "text/xml"); // 单元格 let mergeCell = xmldom.getElementsByTagName("mergeCell"); if (mergeCell.length == 0) return []; let results: string[] = []; for (var i = 0; i < mergeCell.length; i++) { const item = mergeCell[i]; results.push(item.getAttribute('ref') || '') } return results } type imageList = { id: string, target: string, path: string, form?: string, to?: string }[]; // 解析图片列表 const analysisImageList = async (zipLoadResultFiles: { [x: string]: JSZip.JSZipObject }) => { const imageIdKey = 'xl/drawings/_rels/drawing1.xml.rels';//图片存放id文件路径 if (!zipLoadResultFiles[imageIdKey]) return [];//没有图片直接返回空 const fileContent = await zipLoadResultFiles[imageIdKey].async('string'); const parser = new DOMParser(); const xmldom = parser.parseFromString(fileContent, "text/xml"); const list = xmldom.getElementsByTagName("Relationship"); let results: imageList = []; for (var i = 0; i < list.length; i++) { const item = list[i]; results.push({ id: list[i].getAttribute('Id') || '', target: item.getAttribute('Target') || '', path: '' }) } const PromiseList = results.map(item => { return analysisImageBase64(zipLoadResultFiles, item.target) }) await Promise.all(PromiseList).then(res => { res.forEach((item, index) => { results[index].path = item; }) }) return results } // 将图片解析为base const analysisImageBase64 = async (zipLoadResultFiles: { [x: string]: JSZipObject }, keys: string) => { const imageKey = keys.replace('..', 'xl'); const fileContent = await zipLoadResultFiles[imageKey].async('base64'); const url = `data:image/png;base64,${fileContent}`; return url; } // 解析图标坐标 const analysisImageLocation = async (zipLoadResultFiles: { [x: string]: JSZip.JSZipObject }, imageList: imageList) => { if (imageList.length === 0) return [] const imageLocationKey = 'xl/drawings/drawing1.xml';//图片坐标文件路径 const fileContent = await zipLoadResultFiles[imageLocationKey].async('string'); let parser = new DOMParser(); let xmldom = parser.parseFromString(fileContent, "text/xml"); // col单元格 let colList = xmldom.getElementsByTagName("xdr:col"); // row单元格 let rowList = xmldom.getElementsByTagName("xdr:row"); // 图片 let blip = xmldom.getElementsByTagName("a:blip"); let locationList = [] as { form: string, to: string, id: string, path: string }[]; for (var i = 0; i < blip.length; i++) { const formX = Number(colList[i * 2].textContent); const toX = Number(colList[i * 2 + 1].textContent); const formY = Number(rowList[i * 2].textContent) + 1; const toY = Number(rowList[i * 2 + 1].textContent) + 1 const id = blip[i].getAttribute('r:embed'); const path = imageList.filter(i => i.id == id)[0].path; locationList.push({ form: rowKeys.value[formX] + '' + formY, to: rowKeys.value[toX] + '' + toY, id: blip[i].getAttribute('r:embed') || '', path }); } return locationList } // 解析数据 const getExcelData = (file: Blob) => { let fileReader = new FileReader(); // 构建fileReader对象 fileReader.readAsArrayBuffer(file); // 读取指定文件内容 // 读取操作完成时 fileReader.onload = function (e) { try { let data = e.target?.result; // 取得数据data let workbook = XLSX.read(data, { type: "binary" }); // 将data转换成excel工作表数据 const worksheet = workbook.Sheets[workbook.SheetNames[0]]; // 获取第一个工作表 /* * XLSX.utils.sheet_to_json 输出JSON格式数据 * 获取指定工作表中的数据sheetlist[],整个表中的数据存放在一个数组sheetlist中; * sheetlist数组中的每个元素均为一个数组rowlist,是每一行的数据; * header 如果列太多,需要修改列的长度数据 可以使用默认值 1 */ const sheetlist = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); /** * 封装数据 **********#######********* */ formatDate(sheetlist); } catch (e) { console.log("文件类型不正确"); return; } }; } const countCell = (name: string) => { let startX = name.match(/[A-Za-z]+/g)?.join('') || '' let startY = name.replace(startX, ''); return { xEn: startX, x: rowKeys.value.findIndex(i => i == startX), w: name, y: Number(startY), } } onMounted(() => { }); // 封装数据 function formatDate(sheetlist: string | any[]) { if (sheetlist.length < 1) return; // 这个很重要,根据列来设置(有几列就设置几个字段) const keys = tabskeys.value // 处理数据 const setEmptyList = (ul: string | any[]) => { let obj = {} as { [x: string]: any }; for (let i = 0; i < ul.length; i++) { const item = ul[i]; obj[keys[i]] = item === undefined ? null : item } return obj } // 合并的单元格(根据第一列来处理数据,因此,传入的格式上,第一列一定是序号,因此先过滤拿出存在A的列表数据) let celllist = tabCellList.value as any[]; let existList: { data: any, list: string[] }[] = [];//合并的单元格数据 // 计算合并的单元格有哪些 celllist.forEach(item => { const [s, e] = item.split(':'); let start = countCell(s); let end = countCell(e); let xNumber = end.x - start.x; let l = { data: '', list: [] as string[] }; for (let i = 0; i <= xNumber; i++) { let enx = rowKeys.value[start.x + i]; for (let j = start.y; j <= end.y; j++) { l.list.push(enx + '' + j); } } existList.push(l); }); // 判断图片是否在合并的单元格中(如果在,那么直接改data) existList.length > 0 && (existList = existList.map(item => { const tableImage = tableImages.value.filter(items => { return item.list.findIndex(e => e == items.form) > -1 }).map(item => { return item }) tableImage.length > 0 && (item.data = tableImage) return item })) const dataList = []; // Y轴 for (let index = 0; index < sheetlist.length; index++) { const item = sheetlist[index]; // X轴 for (let j = 0; j < item.length; j++) { const enString = rowKeys.value[j] + '' + (index + 1); // 判断当前的单元格是否被合并 const isExistList = existList.filter(item => item.list.some(e => e == enString)); // 该单元格被合并了(一般合并了之后数据会为empty) if (isExistList.length > 0) { // 该单元格等于合并的第一个单元格(则记录第一个单元格的数据)(计算table合并数据缩进) if (isExistList[0].list[0] === enString) { existList = existList.map(item => { !item.data && item.list[0] == enString && (item.data = sheetlist[index][j]) return item }); !sheetlist[index][j] && (sheetlist[index][j] = isExistList[0].data) } else { sheetlist[index][j] = isExistList[0].data } } // 计算图片的左上角是否在该单元格中,如果在,那么赋值 else { const tableImage = tableImages.value.filter(items => { return enString === items.form; }).map(item => { return item }) tableImage.length > 0 && (sheetlist[index][j] = tableImage) } } const countObj = setEmptyList(sheetlist[index]) dataList.push(countObj); } // 处理表头 const setTableColumn = (list: any[]) => { return list.map((item: any, index: number) => { return { label: item, prop: keys[index] } }) } tableColumnLabel.value = setTableColumn(sheetlist[0]); // 使用第一行作为获取表头 dataList.splice(0, 1); tableData.value = JSON.parse(JSON.stringify(dataList)); console.log(tableData.value); } </script> <style lang="scss" scoped></style>
原理就是通过jszip将excel解压,解压后的文件目录为
通过查看里面文件可以找到记录图片坐标,记录图片的xml,还有图片的路径
使用DOMparser解析出xml文件,获取需要的数据,在计算出图片所在的位置即可