vue3 前端解析带图片的excel
将带图片的excel上传后解析数据,然后将数据放入表格。主要是为了将数据传给服务端
目前存在的问题
1、只能解析第一个表里面的内容
2、最后得到的数据需要自己处理一下
需要注意
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));
}
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文件,获取需要的数据,在计算出图片所在的位置即可