当前位置:首页 > 技术 > 正文内容

Vue 前端实现 TXT 与 Excel 文件导出的工程化方案

访客 技术 2026年6月18日 1

一、 导出纯文本(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)
      }));
    }
  }
}

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。