Vue3+TypeScript+ Element Plus 从Excel文件导入数据,无后端(点击按钮,选择Excel文件,由前端解析数据)

发布于:2025-06-21 ⋅ 阅读:(14) ⋅ 点赞:(0)

在 Vue 3 + TypeScript + Element Plus 中实现文件导入功能,可以通过以下步骤完成:

1. 安装依赖

bash

复制

下载

npm install xlsx  # 用于解析Excel文件
npm install @types/xlsx -D  # TypeScript类型声明

2. 组件实现

vue

复制

下载

<template>
  <div>
    <!-- 隐藏的原生文件输入 -->
    <input 
      type="file"
      ref="fileInput"
      @change="handleFileChange"
      accept=".xlsx, .xls, .csv"
      style="display: none"
    >
    
    <!-- Element Plus 按钮触发文件选择 -->
    <el-button 
      type="primary"
      @click="triggerFileInput"
    >
      <el-icon><upload /></el-icon>
      导入文件
    </el-button>
    
    <!-- 显示导入数据 -->
    <el-table :data="tableData" v-if="tableData.length">
      <el-table-column 
        v-for="(header, index) in tableHeaders" 
        :key="index"
        :prop="header"
        :label="header"
      />
    </el-table>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import * as XLSX from 'xlsx';
import type { UploadInstance } from 'element-plus';
import { ElMessage } from 'element-plus';

// 类型定义
interface TableData {
  [key: string]: any;
}

// 响应式数据
const fileInput = ref<HTMLInputElement | null>(null);
const tableData = ref<TableData[]>([]);
const tableHeaders = ref<string[]>([]);

// 触发文件选择
const triggerFileInput = () => {
  if (fileInput.value) {
    fileInput.value.value = ''; // 重置以允许重复选择相同文件
    fileInput.value.click();
  }
};

// 处理文件选择
const handleFileChange = (e: Event) => {
  const input = e.target as HTMLInputElement;
  if (!input.files?.length) return;

  const file = input.files[0];
  const reader = new FileReader();

  reader.onload = (e) => {
    try {
      const data = e.target?.result as ArrayBuffer;
      processExcel(data);
    } catch (error) {
      console.error('文件解析失败:', error);
      ElMessage.error('文件解析失败');
    }
  };

  reader.readAsArrayBuffer(file);
};

// 处理Excel数据
const processExcel = (data: ArrayBuffer) => {
  const workbook = XLSX.read(data, { type: 'array' });
  const firstSheetName = workbook.SheetNames[0];
  const worksheet = workbook.Sheets[firstSheetName];
  
  // 转换为JSON
  const jsonData: TableData[] = XLSX.utils.sheet_to_json(worksheet);
  
  if (jsonData.length > 0) {
    // 获取表头
    tableHeaders.value = Object.keys(jsonData[0]);
    tableData.value = jsonData;
    ElMessage.success(`成功导入 ${jsonData.length} 条数据`);
  } else {
    ElMessage.warning('未找到有效数据');
  }
};
</script>

3. 功能说明

  1. 文件选择触发

    • 隐藏原生 <input type="file"> 元素

    • 通过Element Plus按钮触发文件选择

  2. 文件处理流程

    • 使用 FileReader 读取文件内容

    • 通过 xlsx 库解析Excel数据

    • 将工作表转换为JSON格式

    • 提取表头和数据

  3. 数据展示

    • 使用Element Plus的 <el-table> 动态渲染数据

    • 自动识别表头生成列

4. 扩展功能建议

  1. 文件类型验证

ts

复制

下载

// 在handleFileChange中添加
const validTypes = [
  'application/vnd.ms-excel',
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  'text/csv'
];

if (!validTypes.includes(file.type)) {
  ElMessage.error('请上传Excel或CSV文件');
  return;
}
  1. 数据清洗

ts

复制

下载

// 在processExcel后添加数据清洗逻辑
const cleanData = jsonData.map(item => {
  // 示例:移除空值
  return Object.fromEntries(
    Object.entries(item).filter(([_, value]) => value !== null)
  );
});
  1. 上传到服务器

ts

复制

下载

import axios from 'axios';

const uploadData = async () => {
  try {
    const response = await axios.post('/api/import', {
      data: tableData.value,
      headers: tableHeaders.value
    });
    ElMessage.success(`服务器导入成功: ${response.data.message}`);
  } catch (error) {
    ElMessage.error('服务器导入失败');
  }
};

5. 注意事项

  1. 大文件处理

    • 添加文件大小限制

    • 使用Web Worker防止界面卡顿

    • 分片读取处理

  2. 安全性

    • 验证文件内容格式

    • 防止XSS攻击(特别是CSV文件)

    • 服务器端二次验证

  3. 用户体验

    • 添加加载状态

    • 显示解析进度

    • 错误文件类型提示

vue

复制

下载

<!-- 加载状态示例 -->
<el-button 
  :loading="isLoading" 
  @click="triggerFileInput"
>
  导入文件
</el-button>

这个实现方案提供了完整的文件导入流程,包括前端解析和展示,可根据实际需求扩展服务器上传功能。

6.实例代码

点击按钮,选择Excel文件,由前端解析数据,实现从Excel文件导入数据

1、导入的黄金搭档【按钮 + 输入框】,按钮显示充门面,输入框隐藏干实事

2、导入核心功能封装成工具

在组件中使用

ReagentInDialog.vue

<script setup lang="ts" name="ReagentInDialog">


import { importExcelFileByClient } from "@/utils/excelUtils";

// 文件输入实例对象
const fileInputRef = ref<HTMLInputElement | null>(null);

// 导入
const onImportClick = () => {
  // 模拟点击元素
  if (fileInputRef.value) {
    // 重置以允许重复选择相同文件
    fileInputRef.value.value = "";
    fileInputRef.value.click();
  }
};

// 点击【导入】触发
const handleImportByClient = async (e: Event) => {
  // 获取文件对象
  const input = e.target as HTMLInputElement;
  if (!input.files?.length) return;
  const file = input.files[0];

  // 键值列名映射表
  const keyColMap: Record<string, string> = {
    编号: "materialNo",
    试剂编号: "reagentNo",
    试剂名称: "reagentName",
    规格型号: "reagentSpec",
    单位: "reagentUnit",
    批号: "batchNo",
    有效期至: "validityDate",
    入库数量: "amount",
    入库金额: "total"
  };

  // 导入文件,由前端解析文件,获取数据
  const dataList = <IReagentInByCkDetail[]>await importExcelFileByClient(file, keyColMap);

  // 加载数据
  dataList.forEach((item) => {
    tableData.value.push({
      id: -(tableData.value.length + 1),
      materialNo: (tableData.value.length + 1).toString(),
      reagentNo: item.reagentNo,
      reagentName: item.reagentName
    });
  });

  // 等待 DOM 渲染完毕
  await nextTick();

  // 全选
  tableRef.value?.toggleAllSelection();
};

</script>

<template>

                  <el-button class="in-btn" type="primary" plain @click="onImportClick">导入</el-button>
                  <!-- 文件输入元素,不显示,通过点击按钮【导入】执行 onImportClick,模拟点击该元素,从而触发 handleImportByClient 事件 -->
                  <input
                    ref="fileInputRef"
                    type="file"
                    accept=".xls, .xlsx"
                    style="display: none"
                    @change="handleImportByClient" />

</template>

导入工具

excelUtils.ts

import { convertFileSize } from "@/utils/pubUtils";
import { ElMessage } from "element-plus";
import * as xlsx from "xlsx";


/**
 * 从Excel文件导入数据,由前端解析文件,获取数据
 * @param file 导入文件
 * @param colKeyMap 列名键值映射,key --> value,如:excel中列名为【样品编号】,其键值设置对应为【sampleNo】
 * @returns 列表数据
 */
export async function importExcelFileByClient(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;
  }

  // 文件读取
  let fileReader = new FileReader();
  // 以二进制的方式读取文件内容
  fileReader.readAsArrayBuffer(file);
  // 等待打开加载完成文件,其实就是执行 fileReader.onloadend = () => {},返回 true 表示成功,false 表示失败
  let result = await loadedFile(fileReader);
  if (result) {
    // 获取文件数据
    let fileData = fileReader.result;
    // 读取工作薄 workbook
    let workbook = xlsx.read(fileData, { type: "array" });
    // 表格是有序列表,因此可以取多个 Sheet,这里取第一个 Sheet
    let sheet = workbook.SheetNames[0];
    // 将表格内容生成 json 数据
    let sheetJson = xlsx.utils.sheet_to_json(workbook.Sheets[sheet]);

    // 限制最多只能导入1000条数据,预防恶意操作导入超大量数据
    if (sheetJson.length > 1000) {
      ElMessage.warning("一次最多只能导入1000条数据!");
      return dataList;
    }

    // 格式化表格json数据 sheetJson,转换成在excel表中看到的那种直观数据
    dataList = formatSheetJson(sheetJson, keyColMap);
  }

  // 返回列表数据
  return dataList;
}

/**
 * 加载文件
 * 是否打开加载了文件,因为 fileReader.onloadend 是异步任务,程序执行时,不会执行完 onloadend 内部的代码再往下执行,
 * 而是执行到 onloadend 内部时,又跳出 onloadend,执行 onloadend 外部的代码
 * 故将 fileReader.onloadend 用 Promise<boolean> 返回对象包裹,程序执行时用await loadedFile,这样就会执行完 onloadend 内部的代码再往下执行
 * 【要让 异步任务 不异步执行,可以用一个方法将其包裹,并且该方法返回Promise对象,执行该方法时用 await】
 * @param fileReader 文件读取器
 * @returns 响应结果
 */
function loadedFile(fileReader: FileReader): Promise<boolean> {
  return new Promise((resolve, reject) => {
    // 读取文件,文件读取完成触发该事件
    fileReader.onloadend = () => {
      try {
        // 成功打开加载完文件数据
        resolve(true);
      } catch (error) {
        // 失败
        reject(false);
      }
    };
  });
}

/**
 * 将表格json数据 sheetJson 转换成列表数据
 * @param sheetJson 表格json数据
 * @param colKeyMap 列名键值映射,key --> value,如:excel中列名为【样品编号】,其键值设置对应为【sampleNo】
 * @returns 列表数据
 */
function formatSheetJson(sheetJson: any[], keyColMap: Record<string, string>): any[] {
  // 无内容,返回空数据
  if (!sheetJson.length) return [];

  let result = sheetJson;
  // 判断是否有表头,有表头的话,sheetJson对象必然有__EMPTY属性
  let hasTableHead = !!sheetJson[0]["__EMPTY"];
  // 拥有表头的数据,重新转换列标题
  if (hasTableHead) {
    // 获取对象中所有属性的名称
    let header = sheetJson.shift();
    // 数据
    let data: any[] = [];
    // 遍历对象所有属性(列信息)
    Object.keys(header).forEach((key) => {
      // 遍历数据(行信息)
      sheetJson.forEach((item, index) => {
        // 构建对象内容
        let obj = data[index] || {};
        // 对象增加属性,并给属性赋值数据(行列信息)
        obj[header[key]] = item[key];
        // 最终给数据行数据赋值对象内容
        data[index] = obj;
      });
    });
    result = data;
  }

  // 将表格对应的文字转换为 key
  let dataList: any[] = [];
  result.forEach((item) => {
    let newItem: any = {};
    Object.keys(item).forEach((key) => {
      newItem[keyColMap[key]] = item[key];
    });
    dataList.push(newItem);
  });

  // 返回列表数据
  return dataList;
}


网站公告

今日签到

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