【前后前】导入Excel文件闭环模型:Vue3前端上传Excel文件,【Java后端接收、解析、返回数据】,Vue3前端接收展示数据

发布于:2025-06-23 ⋅ 阅读:(20) ⋅ 点赞:(0)

【前后前】导入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事件 -->
                  <input
                    ref="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)");
            }

            // 解析 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

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

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

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 

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 "";
        }
    }
}

 


网站公告

今日签到

点亮在社区的每一天
去签到