在 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];// 转换为JSONconst 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. 功能说明
-
文件选择触发:
-
隐藏原生
<input type="file">
元素 -
通过Element Plus按钮触发文件选择
-
-
文件处理流程:
-
使用
FileReader
读取文件内容 -
通过
xlsx
库解析Excel数据 -
将工作表转换为JSON格式
-
提取表头和数据
-
-
数据展示:
-
使用Element Plus的
<el-table>
动态渲染数据 -
自动识别表头生成列
-
4. 扩展功能建议
-
文件类型验证:
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; }
-
数据清洗:
ts
复制
下载
// 在processExcel后添加数据清洗逻辑 const cleanData = jsonData.map(item => {// 示例:移除空值return Object.fromEntries(Object.entries(item).filter(([_, value]) => value !== null)); });
-
上传到服务器:
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. 注意事项
-
大文件处理:
-
添加文件大小限制
-
使用Web Worker防止界面卡顿
-
分片读取处理
-
-
安全性:
-
验证文件内容格式
-
防止XSS攻击(特别是CSV文件)
-
服务器端二次验证
-
-
用户体验:
-
添加加载状态
-
显示解析进度
-
错误文件类型提示
-
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 事件 --><inputref="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;// 读取工作薄 workbooklet workbook = xlsx.read(fileData, { type: "array" });// 表格是有序列表,因此可以取多个 Sheet,这里取第一个 Sheetlet 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;}// 将表格对应的文字转换为 keylet dataList: any[] = [];result.forEach((item) => {let newItem: any = {};Object.keys(item).forEach((key) => {newItem[keyColMap[key]] = item[key];});dataList.push(newItem);});// 返回列表数据return dataList;
}