Vue 前端实现 TXT 与 Excel 文件导出的工程化方案
一、 导出纯文本(TXT)文档
在前端导出纯文本文件时,核心思路是将业务数据格式化为多行字符串,利用 Blob 对象构建文件流,并通过动态创建锚点(<a>)标签来触发浏览器的原生下载行为。
1. 视图层交互绑定
在模板中定义触发导出的按钮,并绑定点击事件。
<template>
<div class="export-actions">
<el-button type="primary" :loading="isGeneratingTxt" @click="handleExportTxt">
导出 TXT 报告
</el-button>
</div>
</template>
2. 逻辑层重构与优化
传统的字符串拼接(+=)在处理大量数据时不仅性能较差,且代码可读性低。以下重构方案采用了 ES6 模板字符串进行排版,并使用 Array.prototype.reduce 进行数据聚合统计,大幅提升了代码的整洁度与执行效率。
methods: {
async handleExportTxt() {
this.isGeneratingTxt = true;
try {
const siteIndex = this.selectedSiteId - 1;
const siteName = this.siteList[siteIndex].name;
const reportDate = moment(this.targetDate).format("YYYY-MM-DD");
// 使用 reduce 进行高效的数据聚合
const stats = this.vehicleRecords.reduce((acc, curr) => {
const dayUsage = parseFloat(curr.dayShift.usageRate);
const nightUsage = parseFloat(curr.nightShift.usageRate);
acc.dayRunTime += curr.dayShift.totalRunTime;
acc.dayGrossTime += curr.dayShift.totalRunTime + curr.idle.dayShift.chargeTime;
acc.nightRunTime += curr.nightShift.totalRunTime;
acc.nightGrossTime += curr.nightShift.totalRunTime + curr.idle.nightShift.chargeTime;
acc.dayTripCount += curr.dayShift.transportQty;
acc.nightTripCount += curr.nightShift.transportQty;
if (dayUsage === 0 && nightUsage === 0) {
acc.idleVehicles.push(curr.vehicleName);
} else {
acc.activeVehicles.push(curr.vehicleName);
if (dayUsage > 0) {
acc.dayActiveCount++;
acc.dayActiveVehicles.push(curr.vehicleName);
}
if (nightUsage > 0) {
acc.nightActiveCount++;
acc.nightActiveVehicles.push(curr.vehicleName);
}
}
return acc;
}, {
dayRunTime: 0, dayGrossTime: 0, nightRunTime: 0, nightGrossTime: 0,
dayTripCount: 0, nightTripCount: 0, dayActiveCount: 0, nightActiveCount: 0,
activeVehicles: [], idleVehicles: [], dayActiveVehicles: [], nightActiveVehicles: []
});
// 使用模板字符串构建报告内容
const textContent = `
${reportDate} ${siteName} 车辆运营情况报告
=========================================
投入使用的车辆共有 ${stats.activeVehicles.length} 辆,分别为: ${stats.activeVehicles.join(', ')}
未投入使用车辆共有 ${stats.idleVehicles.length} 辆,分别为: ${stats.idleVehicles.join(', ')}
【白班运营数据】
共有 ${stats.dayActiveCount} 台车辆投入使用,分别为: ${stats.dayActiveVehicles.join(', ')}
运行总时长: ${this.utils.formatDuration(stats.dayRunTime)}
车辆平均使用率: ${this.calcUsageRate(stats.dayRunTime, stats.dayActiveCount)}%
出入库总次数: ${stats.dayTripCount} 次
【晚班运营数据】
共有 ${stats.nightActiveCount} 台车辆投入使用,分别为: ${stats.nightActiveVehicles.join(', ')}
运行总时长: ${this.utils.formatDuration(stats.nightRunTime)}
车辆平均使用率: ${this.calcUsageRate(stats.nightRunTime, stats.nightActiveCount)}%
出入库总次数: ${stats.nightTripCount} 次
=========================================
以下为各车辆运行详细数据:
${this.generateVehicleDetails(this.vehicleRecords)}
`.trim();
const blob = new Blob([textContent], { type: "text/plain;charset=utf-8" });
this.triggerFileDownload(blob, `${reportDate}_${siteName}_运营报告.txt`);
} catch (error) {
this.$message.error("生成 TXT 报告失败");
} finally {
this.isGeneratingTxt = false;
}
},
calcUsageRate(totalTime, vehicleCount) {
if (vehicleCount === 0) return "0.00";
return ((totalTime * 100) / this.baseTime / 3600 / vehicleCount).toFixed(2);
},
generateVehicleDetails(records) {
return records.filter(item => {
return parseFloat(item.dayShift.usageRate) > 0 || parseFloat(item.nightShift.usageRate) > 0;
}).map(item => {
let detail = `\n[${item.vehicleName}号车]\n`;
if (parseFloat(item.dayShift.usageRate) > 0) {
detail += `白班 - 运行时长: ${this.utils.formatDuration(item.dayShift.totalRunTime)}, 任务次数: ${item.dayShift.transportQty}\n`;
}
if (parseFloat(item.nightShift.usageRate) > 0) {
detail += `晚班 - 运行时长: ${this.utils.formatDuration(item.nightShift.totalRunTime)}, 任务次数: ${item.nightShift.transportQty}\n`;
}
return detail;
}).join('');
},
triggerFileDownload(blob, fileName) {
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
}
二、 导出 Excel 报表
对于结构化的表格数据,Excel 是更优的载体。早期的 Vue 项目常使用 require.ensure 配合自定义的 Export2Excel.js 脚本,这种方式不仅难以维护,且缺乏类型支持。现代工程化方案推荐使用 xlsx (SheetJS) 结合 file-saver,并配合 async/await 语法来处理异步数据流。
1. 视图层交互绑定
<template>
<div class="export-actions">
<el-button type="success" :loading="isExportingExcel" @click="handleExportExcel">
导出 Excel 报表
</el-button>
</div>
</template>
2. 逻辑层重构与优化
在触发导出前,需对时间范围和站点选择进行严格校验。数据获取后,通过映射函数将后端返回的 JSON 转换为符合 Excel 表头结构的二维数组,最后利用 XLSX 库生成二进制流并下载。
import * as XLSX from 'xlsx';
import { saveAs } from 'file-saver';
export default {
data() {
return {
isExportingExcel: false
};
},
methods: {
async handleExportExcel() {
if (!this.selectedSiteId || !this.startDate || !this.endDate) {
return this.$message.warning("请选择基地并设置有效的时间范围");
}
if (this.endDate.getTime() < this.startDate.getTime()) {
return this.$message.warning("结束时间不能早于开始时间");
}
this.isExportingExcel = true;
try {
const params = {
siteId: this.selectedSiteId,
startDate: moment(this.startDate).format("YYYY/MM/DD"),
endDate: moment(this.endDate).format("YYYY/MM/DD")
};
const response = await api.fetchVehicleHistory(params);
if (!response || response.length === 0) {
return this.$message.info("当前查询条件下无数据可导出");
}
const siteName = this.siteList[this.selectedSiteId - 1].name;
const excelData = this.transformExcelData(response, siteName);
// 定义表头与字段映射
const headers = [
"基地名称", "车辆编号", "总运行时长", "任务时长", "任务次数",
"充电时长", "充电次数", "停车时长", "停车次数", "空闲充电时长", "空闲停车时长"
];
const keys = [
"siteName", "vehicleId", "totalRunTime", "taskTime", "taskCount",
"chargeTime", "chargeCount", "parkTime", "parkCount", "idleChargeTime", "idleParkTime"
];
// 构建工作表数据 (包含表头)
const wsData = [headers, ...excelData.map(row => keys.map(k => row[k]))];
const worksheet = XLSX.utils.aoa_to_sheet(wsData);
// 设置列宽
worksheet['!cols'] = headers.map(() => ({ wch: 15 }));
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "历史运营数据");
const excelBuffer = XLSX.write(workbook, { bookType: "xlsx", type: "array" });
const blob = new Blob([excelBuffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
});
const fileName = `基地历史数据_${params.startDate}_至_${params.endDate}.xlsx`;
saveAs(blob, fileName);
} catch (error) {
console.error("Excel 导出异常:", error);
this.$message.error("导出 Excel 失败,请稍后重试");
} finally {
this.isExportingExcel = false;
}
},
transformExcelData(rawData, siteName) {
return rawData.map(item => ({
siteName: siteName,
vehicleId: item.vehicleName,
totalRunTime: this.utils.formatDuration(item.whiteAndNightData.allRunTime),
taskTime: this.utils.formatDuration(item.whiteAndNightData.transportTime),
taskCount: item.whiteAndNightData.transportQty,
chargeTime: this.utils.formatDuration(item.whiteAndNightData.chargeTime),
chargeCount: item.whiteAndNightData.chargeQty,
parkTime: this.utils.formatDuration(item.whiteAndNightData.parkTime),
parkCount: item.whiteAndNightData.parkQty,
idleChargeTime: this.utils.formatDuration(item.noTask.whiteAndNightData.chargeTime),
idleParkTime: this.utils.formatDuration(item.noTask.whiteAndNightData.parkTime)
}));
}
}
}