在 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. 功能说明
文件选择触发:
隐藏原生
<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 事件 -->
<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;
}