vue3 前端解析带图片的excel

symlove / 2024-10-21 / 原文

将带图片的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文件,获取需要的数据,在计算出图片所在的位置即可