【前后前】导入Excel文件闭环模型:Vue3前端上传Excel文件,【Java后端接收、解析、返回数据】,Vue3前端接收展示数据
一、Vue3前端上传(导入)Excel文件
ReagentInDialog.vue
TypeScript
<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事件 -->
<input
ref="fileInputRef"
type="file"
accept=".xls, .xlsx"
style="display: none"
@change="handleImport" />
</template>
excelUtils.ts
TypeScript
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
TypeScript
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
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)");
}
// 解析 Excel
List<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
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)");
// }
// 解析 Excel
List<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
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 {
@Autowired
private 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
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 文件
@Override
public List<Map<String, Object>> parseExcelFile(MultipartFile uploadFile) {
try {
// 解析 Excel
return ExcelUtils.parseExcel(uploadFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
excel文件处理工具类 ExcelUtils.java
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 "";
}
}
}