【前后前】导入Excel文件闭环模型:Vue3前端上传Excel文件,【Java后端接收、解析、返回数据】,Vue3前端接收展示数据
【前后前】导入Excel文件闭环模型:Vue3前端上传Excel文件,【Java后端接收、解析、返回数据】,Vue3前端接收展示数据
一、Vue3前端上传(导入)Excel文件
ReagentInDialog.vue
<script setup lang="ts" name="ReagentInDialog">// 导入
const onImportClick = () => {// 模拟点击元素if (fileInputRef.value) {// 重置以允许重复选择相同文件fileInputRef.value.value = "";fileInputRef.value.click();}
};// 点击【导入】触发
const handleImport= async (e: Event) => {let dataList = [];try {tableLoading.value = true;// 获取文件对象const input = e.target as HTMLInputElement;if (!input.files?.length) return;const file = input.files[0];// 键值列名映射表const keyColMap: Record<string, string> = {试剂编号: "reagentNo",试剂名称: "reagentName",规格型号: "reagentSpec",单位: "reagentUnit",批号: "batchNo",有效期至: "validityDate",入库数量: "amount",入库金额: "total"};// 导入文件,由前端解析文件,获取数据// dataList = await importExcelFileByClient(file, keyColMap);// 导入文件,由后端解析文件,获取数据dataList = await importExcelFileByServer(file, keyColMap);} finally {tableLoading.value = false;}
}</script><template><el-button type="primary" plain @click="onImportClick">导入</el-button><!-- 文件输入元素,不显示,通过点击按钮【导入】执行 onImportClick,模拟点击该元素,从而触发 handleImport事件 --><inputref="fileInputRef"type="file"accept=".xls, .xlsx"style="display: none"@change="handleImport" /></template>
excelUtils.ts
import { formatJson } from "@/utils/formatter";
import { convertFileSize } from "@/utils/pubUtils";
import { ElMessage } from "element-plus";
import * as xlsx from "xlsx";
import { uploadFileService } from "@/api/upload";/*** 从Excel文件导入数据,由后端解析文件,获取数据* @param file 导入文件* @param colKeyMap 列名键值映射,key --> value,如:excel中列名为【样品编号】,其键值设置对应为【sampleNo】* @returns 列表数据*/
export async function importExcelFileByServer(file: any, keyColMap?: Record<string, string>) {// 定义及初始化需要返回的列表数据let dataList: any[] = [];// 文件校验// 校验文件名后缀if (!/\.(xls|xlsx)$/.test(file.name)) {ElMessage.warning("请导入excel文件!");return dataList;}// 校验文件格式// application/vnd.ms-excel 为 .xls文件// application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 为 .xlsx文件else if (file.type !== "application/vnd.ms-excel" &&file.type !== "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {ElMessage.warning("excel文件已损坏,请检查!");return dataList;}// 校验文件大小else if (convertFileSize(file.size, "B", "MB") > 1) {ElMessage.warning("文件大小不能超过1MB!");return dataList;}// 文件读取const fileReader = new FileReader();// 以二进制的方式读取文件内容fileReader.readAsArrayBuffer(file);// 等待打开加载完成文件,其实就是执行 fileReader.onloadend = () => {},返回 true 表示成功,false 表示失败const result = await loadedFile(fileReader);if (result) {// 通过 FormData 对象实现文件上传const formData = new FormData();// 将文件对象 file 添加到 formData 对象中,uploadFile 需要与后端接口中接收文件的参数名一致formData.append("uploadFile", file);// 发送请求,上传文件到后端服务器,后端接收文件,进行解析,并返回数据集const result = await uploadFileService(formData);dataList = keyColMap ? formatJson(result.data, keyColMap) : result.data;}// 返回列表数据return dataList;
}
upload.ts
import request from "@/utils/request";/*** 上传文件,后端解析Excel文件,返回解析后的列表数据* @param file 文件,表单数据* @returns 列表数据*/
export const uploadFileService = (file: FormData) => {return request.post("/upload/parseExcelFile", file, {// 上传文件,需设置 headers 信息,将"Content-Type"设置为"multipart/form-data"headers: {"Content-Type": "multipart/form-data"}});
};
二、Java后端接收、解析、返回数据
UploadController.java
package com.weiyu.controller;import com.weiyu.utils.ExcelUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;import java.util.List;
import java.util.Map;/*** 上传 Controller*/
@RestController
@RequestMapping("/upload")
@Slf4j
public class UploadController {// 上传文件,后端解析Excel文件,返回解析后的列表数据// 因为前端是用 "Content-Type": "multipart/form-data" 的方式发送的请求,这里就不能用 @RequestBody,而是用 MultipartFile// 并且形参名称 uploadFile 需要与前端定义的保持一致@PostMapping("/parseExcelFile")public ResponseEntity<?> uploadAndParseExcelFile(MultipartFile uploadFile) {log.info("【上传文件】,解析Excel文件,/upload/parseExcelFile,uploadFile = {}", uploadFile);try {// 验证文件if (uploadFile.isEmpty()) {return ResponseEntity.badRequest().body("文件为空");}// 验证文件类型String contentType = uploadFile.getContentType();if (contentType == null || (!contentType.equals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") && !contentType.equals("application/vnd.ms-excel"))) {return ResponseEntity.badRequest().body("仅支持 Excel 文件 (.xlsx, .xls)");}// 解析 ExcelList<Map<String, Object>> data = ExcelUtils.parseExcel(uploadFile);// 返回解析结果return ResponseEntity.ok(data);} catch (Exception e) {return ResponseEntity.internalServerError().body("解析失败: " + e.getMessage());}}
}
三、Vue3前端接收展示数据
1、正常发送请求数据
2、正常接收响应数据
3、解析出错
四、后端修改方案
UploadController.java
package com.weiyu.controller;import com.weiyu.pojo.Result;
import com.weiyu.utils.ExcelUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;import java.util.List;
import java.util.Map;/*** 上传 Controller*/
@RestController
@RequestMapping("/upload")
@Slf4j
public class UploadController {// 上传文件,后端解析Excel文件,返回解析后的列表数据// 因为前端是用 "Content-Type": "multipart/form-data" 的方式发送的请求,这里就不能用 @RequestBody,而是用 MultipartFile// 并且形参名称 uploadFile 需要与前端定义的保持一致@PostMapping("/parseExcelFile")@ResponseBody // 直接序列化返回值,使用 Result<List<Map<String, Object>>> 替换 ResponseEntity<?>public Result<List<Map<String, Object>>> uploadAndParseExcelFile(MultipartFile uploadFile) {log.info("【上传文件】,解析Excel文件,/upload/parseExcelFile,uploadFile = {}", uploadFile);try {// 验证文件
// if (uploadFile.isEmpty()) {
// return ResponseEntity.badRequest().body("文件为空");
// }// 验证文件类型
// String contentType = uploadFile.getContentType();
// if (contentType == null || (!contentType.equals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") && !contentType.equals("application/vnd.ms-excel"))) {
// return ResponseEntity.badRequest().body("仅支持 Excel 文件 (.xlsx, .xls)");
// }// 解析 ExcelList<Map<String, Object>> data = ExcelUtils.parseExcel(uploadFile);// 返回解析结果
// return ResponseEntity.ok(data);return Result.success(data);} catch (Exception e) {
// return ResponseEntity.internalServerError().body("解析失败: " + e.getMessage());throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "解析失败: " + e.getMessage(), e);}}
}
前端导入效果
五、后端完善方案
UploadController.java
package com.weiyu.controller;import com.weiyu.pojo.Result;
import com.weiyu.service.UploadService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;import java.util.List;
import java.util.Map;/*** 上传 Controller*/
@RestController
@RequestMapping("/upload")
@Slf4j
public class UploadController {@Autowiredprivate UploadService uploadService;// 上传文件,后端解析Excel文件,返回解析后的列表数据// 因为前端是用 "Content-Type": "multipart/form-data" 的方式发送的请求,这里就不能用 @RequestBody,而是用 MultipartFile// 并且形参名称 uploadFile 需要与前端定义的保持一致@PostMapping("/parseExcelFile")// @ResponseBody // 直接序列化返回值,使用 Result<List<Map<String, Object>>> 替换 ResponseEntity<?>public Result<List<Map<String, Object>>> uploadAndParseExcelFile(MultipartFile uploadFile) {log.info("【上传文件】,解析Excel文件,/upload/parseExcelFile,uploadFile = {}", uploadFile);List<Map<String, Object>> data = uploadService.parseExcelFile(uploadFile);return Result.success(data);}
}
UploadServiceImpl.java
package com.weiyu.service.impl;import com.weiyu.service.UploadService;
import com.weiyu.utils.ExcelUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;import java.io.IOException;
import java.util.List;
import java.util.Map;/*** 上传 Service 接口实现*/
@Service
public class UploadServiceImpl implements UploadService {// 解析 Excel 文件@Overridepublic List<Map<String, Object>> parseExcelFile(MultipartFile uploadFile) {try {// 解析 Excelreturn ExcelUtils.parseExcel(uploadFile);} catch (IOException e) {throw new RuntimeException(e);}}
}
excel文件处理工具类 ExcelUtils.java
package com.weiyu.utils;import lombok.extern.slf4j.Slf4j;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.NumberToTextConverter;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.ZoneId;
import java.util.*;/*** excel文件处理工具类*/
@Slf4j
@Component //通过@Component注解,将该工具类交给ICO容器管理,需要使用的时候不需要new,直接@Autowired注入即可
public class ExcelUtils {public static List<Map<String, Object>> parseExcel(MultipartFile file) throws IOException {try (InputStream inputStream = file.getInputStream()) {Workbook workbook = WorkbookFactory.create(inputStream);Sheet sheet = workbook.getSheetAt(0);// 获取表头行Row headerRow = sheet.getRow(0);if (headerRow == null) {return Collections.emptyList();}// 处理表头(处理重复列名)List<String> headers = processHeaders(headerRow);// 解析数据行List<Map<String, Object>> data = new ArrayList<>();for (int i = 1; i <= sheet.getLastRowNum(); i++) {Row row = sheet.getRow(i);if (row == null) continue;Map<String, Object> rowData = parseRow(row, headers);if (!rowData.isEmpty()) {data.add(rowData);}}return data;}}private static List<String> processHeaders(Row headerRow) {List<String> headers = new ArrayList<>();Map<String, Integer> headerCount = new HashMap<>();for (Cell cell : headerRow) {String header = getCellValueAsString(cell).trim();// 处理空表头if (header.isEmpty()) {header = "Column_" + (cell.getColumnIndex() + 1);}// 处理重复表头int count = headerCount.getOrDefault(header, 0) + 1;headerCount.put(header, count);if (count > 1) {header = header + "_" + count;}headers.add(header);}return headers;}private static Map<String, Object> parseRow(Row row, List<String> headers) {Map<String, Object> rowData = new LinkedHashMap<>();DataFormatter formatter = new DataFormatter();for (int i = 0; i < headers.size(); i++) {String header = headers.get(i);Cell cell = row.getCell(i, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);// 根据单元格类型处理数据switch (cell.getCellType()) {case STRING:rowData.put(header, cell.getStringCellValue().trim());break;case NUMERIC:if (DateUtil.isCellDateFormatted(cell)) {// 日期类型处理rowData.put(header, cell.getDateCellValue().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());} else {// 数值类型处理double value = cell.getNumericCellValue();if (value == (int) value) {rowData.put(header, (int) value);} else {rowData.put(header, value);}}break;case BOOLEAN:rowData.put(header, cell.getBooleanCellValue());break;case FORMULA:// 公式单元格处理rowData.put(header, evaluateFormulaCell(cell));break;default:rowData.put(header, formatter.formatCellValue(cell));}}return rowData;}private static Object evaluateFormulaCell(Cell cell) {try {switch (cell.getCachedFormulaResultType()) {case NUMERIC:return cell.getNumericCellValue();case STRING:return cell.getStringCellValue();case BOOLEAN:return cell.getBooleanCellValue();default:return "";}} catch (Exception e) {return "FORMULA_ERROR";}}private static String getCellValueAsString(Cell cell) {if (cell == null) return "";switch (cell.getCellType()) {case STRING:return cell.getStringCellValue();case NUMERIC:return NumberToTextConverter.toText(cell.getNumericCellValue());case BOOLEAN:return String.valueOf(cell.getBooleanCellValue());case FORMULA:return evaluateFormulaCell(cell).toString();default:return "";}}
}