vue3 + xlsx 实现导入导出表格,导出动态获取表头和数据
封装 xlsx.ts 文件
npm i xlsx element-plus
import * as XLSX from "xlsx";
import { ElMessageBox, ElMessage } from "element-plus";
/**
* 导出表格数据为 Excel 文件,自动匹配 el-table 或 vxe-table 表头和数据字段
* element-plus 2.7.6 版本支持动态获取列
* @param {Object} [tableRef] - 表格组件的 ref 引用(可选)
* @param {String} fileName - 导出文件名(默认:export.xlsx)
* @param {Object} options - 配置项(可选)
* @param {Array} options.headers - 自定义表头(可选)
* @param {Array} options.dataKeys - 自定义数据字段(可选)
* @param {Array} options.data - 要导出的数据(可选,如果没有 tableRef 则必须提供)
*
* 使用示例:
* exportExcel(tableRef.value, '用户数据.xlsx')
*
* 自定义表头和数据字段 示例:
* exportExcel(tableRef.value, '用户数据.xlsx', {
headers: ['姓名', '城市'], // 自定义表头
dataKeys: ['name', 'city'] // 自定义数据字段
})
*
* 如果没有 tableRef,则需要提供自定义表头和数据:
* exportExcel(null, '用户数据.xlsx', {
headers: ['姓名', '城市'],
data: [{name: '张三', city: '北京'}, {name: '李四', city: '上海'}]
})
*/
export const exportExcel = (
tableRef: any = null,
fileName = "export.xlsx",
options: any = {}
) => {
let headers = options.headers || [];
let data = options.data || [];
if (tableRef) {
if (tableRef.$options.name === "VxeTable") {
headers =
headers.length > 0
? headers
: tableRef.getColumns().map((col: any) => col.title);
data = data.length > 0 ? data : tableRef.getTableData().fullData;
} else if (tableRef.$options.name === "ElTable") {
headers =
headers.length > 0
? headers
: tableRef.columns.map((col: any) => col.label);
data = data.length > 0 ? data : tableRef.data;
} else {
throw new Error("不支持的表格组件类型");
}
if (options.dataKeys) {
data = data.map((item: any) =>
options.dataKeys.reduce(
(obj: any, key: any) => ({ ...obj, [key]: item[key] }),
{}
)
);
}
}
if (!headers.length) {
throw new Error("缺少必要的表头");
}
// 构建工作表数据
const worksheetData = [
headers, // 表头行
...data.map((item: any) =>
headers.map((header: any) => {
const key = Object.keys(item).find(
(k) => k.toLowerCase() === header.toLowerCase()
);
return key ? item[key] : "";
})
),
];
// 创建 workbook
const workbook = XLSX.utils.book_new();
const worksheet = XLSX.utils.aoa_to_sheet(worksheetData);
// 添加样式
worksheet["!cols"] = headers.map(() => ({ wch: 10 })); // 列宽
// 组合并导出
XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1");
XLSX.writeFile(workbook, fileName);
};
/**
* 从Excel文件导入数据,并可以选择性地更新指定表格组件的数据。
* @param {Object} [tableRef] - 表格组件的 ref 引用(可选)
* @param {string[]} [requiredFields] - 必填字段的表头名称数组(可选)
* @returns {Promise<any[]>} 解析后的数据数组
*
* 示例:仅导入数据
* importExcel().then(data => console.log('导入的数据:', data));
*
* 示例:导入数据并更新指定的表格组件
* importExcel(tableRef).then(() => console.log('表格已更新'));
*
* 示例:导入数据并校验必填字段
* importExcel(null, ['名称']).then(data => console.log('导入的数据:', data));
*/
export const importExcel = (
tableRef: any = null,
requiredFields: string[] = []
) => {
return new Promise((resolve, reject) => {
const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", ".xlsx"); // 限制只能选择 .xlsx 文件
input.click();
input.onchange = (e: any) => {
const files = e.target.files;
if (!files.length) {
reject(new Error("没有选择文件"));
return;
}
const file = files[0];
// 校验文件类型
if (!file.name.endsWith(".xlsx")) {
reject(new Error("只能导入 .xlsx 文件"));
return;
}
const reader = new FileReader();
reader.onload = (event: any) => {
try {
const data = new Uint8Array(event.target.result);
const workbook = XLSX.read(data, { type: "array" });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
// 提取并校验表头
const headerData: any = XLSX.utils.sheet_to_json(worksheet, {
header: 1,
})[0]; // 获取第一行作为表头
if (!requiredFields.every((field) => headerData.includes(field))) {
reject(new Error("导入的表格表头与要求不符,请使用正确的模板!"));
return;
}
// 直接转换数据(自动跳过表头行)
const newData = XLSX.utils.sheet_to_json(worksheet, {
defval: "", // 将空值默认为空字符串
});
// 校验必填字段
if (requiredFields.length > 0) {
const missingFieldsMap = new Map(); // 用于记录缺失必填字段的行号
newData.forEach((row: any, index: number) => {
requiredFields.forEach((field) => {
const fieldValue = row[field]; // 获取字段值
// 判断字段值是否为空(允许 0 和 false)
if (
fieldValue === "" ||
fieldValue === null ||
fieldValue === undefined
) {
// 记录缺失字段的行号(index + 2,因为表头占一行,且数组从0开始)
const rowNumber = index + 2;
if (!missingFieldsMap.has(rowNumber)) {
missingFieldsMap.set(rowNumber, []);
}
missingFieldsMap.get(rowNumber).push(field);
}
});
});
// 如果有缺失字段,弹出错误提示
if (missingFieldsMap.size > 0) {
let errorMessage = "以下行的必填字段缺失:<br>"; // 使用 <br> 换行
missingFieldsMap.forEach((fields, rowNumber) => {
errorMessage += `第 ${rowNumber} 行缺失字段:${fields.join(
", "
)}<br>`; // 使用 <br> 换行
});
reject(new Error(errorMessage)); // 同时 reject Promise
return;
}
}
// 如果提供了 tableRef,则更新表格数据
if (tableRef) {
let existingData;
if (tableRef.$options.name === "VxeTable") {
existingData = tableRef.getTableData().fullData;
} else if (tableRef.$options.name === "ElTable") {
existingData = tableRef.data;
}
const combinedData = [...existingData, ...newData];
if (tableRef.$options.name === "VxeTable") {
tableRef.loadData(combinedData);
} else if (tableRef.$options.name === "ElTable") {
tableRef.data = combinedData;
}
}
resolve(newData); // 返回不包含表头的数据
} catch (error) {
reject(error);
}
};
reader.onerror = () => reject(new Error("读取文件时出错"));
reader.readAsArrayBuffer(file);
};
});
};
/**
* 导入数据并插入到数据库
* @param {any[]} data - 导入的数据
* @param {Function} insertApi - 插入数据的接口函数
* @param {Function} refreshTable - 刷新表格的函数
* @param {Function} setLoading - 控制 loading 状态的函数
*/
export const importAndInsertData = async (
data: any[],
insertApi: (item: any) => Promise<any>,
refreshTable: () => void,
setLoading: (isLoading: boolean) => void
) => {
try {
setLoading(true);
// 存储所有插入操作的 Promise
const insertPromises = data.map((item) =>
insertApi(item).catch((error) => {
// 如果插入失败,返回失败的数据和错误信息
return { item, error };
})
);
// 等待所有插入操作完成
const results = await Promise.all(insertPromises);
// 检查是否有插入失败的数据
const failedItems = results.filter((result) => result && result.error);
if (failedItems.length > 0) {
// 如果有插入失败的数据,提示失败的具体信息
let errorMessage = "以下数据插入失败:<br>";
failedItems.forEach(({ item, error }) => {
errorMessage += `数据:${JSON.stringify(item)},错误:${
error.message
}<br>`;
});
ElMessageBox.alert(errorMessage, "导入失败", {
confirmButtonText: "确定",
dangerouslyUseHTMLString: true,
});
} else {
// 如果全部插入成功,提示成功并刷新表格
ElMessage.success("导入成功");
refreshTable();
}
} catch (error: any) {
// 捕获全局错误
ElMessageBox.alert(`导入过程中发生错误:${error.message}`, "导入失败", {
confirmButtonText: "确定",
});
} finally {
setLoading(false); // 无论成功或失败,最终关闭 loading
}
};
/**
* importAndInsertData 函数使用示例
* 处理导入数据的函数
const handleImport = async () => {
try {
// 必填字段
let requiredFields = ['费用类型(ID)', '加成率(ID)', '名称', '拼音码', '规格', '价格', '最小单位', '库房转换率(整数)', '进货单位', '五笔码', '库房地点(ID)']
const data: any = await importExcel(null, requiredFields);
if (data.length === 0) {
ElMessage.warning("导入的数据为空");
return;
}
// 定义插入数据的接口函数
const insertApi = async (item: any) => {
const form = {
code_item_cls: 0,
code_item_id: 0,
}
const response = await api(form);
if (response.data[0].success != 'T') {
throw new Error(response.message || "插入数据失败");
}
return response;
};
// 调用导入并插入数据的函数
await importAndInsertData(data, insertApi, handleLoadChargeItemlist, (isLoading) => {
importLoading.value = isLoading; // 控制 loading 状态
});
} catch (error: any) {
ElMessageBox.alert(error.message, "导入失败", {
confirmButtonText: "确定",
dangerouslyUseHTMLString: true,
});
}
};
*/