Vue3 + TypeScript + Element Plus + el-table 表格数据导出Excel文件(通过组合式函数 hook + 工具 封装实现)

表格数据导出Excel文件

1、使用 hook 封装,可复用,使用极简

2、定义 ElTable 扩展实例类型,包含内部 store 属性,使用代码通过 TypeScript 的检查

3、由前端导出,使用 xlsx 工具库,xlsx 是一个优秀的表格处理库,是一款适用于浏览器和 nodejs 的开源电子表格解析库

4、支持暂无数据的导出,并且带表头信息(导出就是模板)

组合式函数 hook

/hooks/useElTableExtendedInstance.ts

import type { ElTableExtendedInstance } from "@/interface";
import { exportExcelFile } from "@/utils/excelUtils";
import { ref, type Ref } from "vue";

/**
 * ElTable 扩展实例 hook
 * 该 hook 的作用是处理 ElTable 的列属性和列标签之间的映射关系
 * @param tableRef 表格实例的引用,用于访问和操作表格的属性和方法
 * @returns 返回一个对象,包含列属性和列标签的键值对以及获取这些键值对的方法
 */
export const useElTableExtendedInstance = (tableRef: Ref<ElTableExtendedInstance | null>) => {
  // 列属性和列标签的映射表
  const columnPropertyLabelMap = ref<Record<string, string>>({});

  /**
   * 获取当前表格的有效列数组
   * 如果表格实例或其状态无效,则返回 null
   */
  const getTableColumns = (): Array<{ property: string; label: string }> | null => {
    const instance = tableRef.value;
    if (!instance || !instance.store || !instance.store.states || !instance.store.states.columns) {
      return null;
    }
    const columns = instance.store.states.columns.value;
    return Array.isArray(columns) ? columns : null;
  };

  /**
   * 安全地从对象中获取指定属性的值
   * 避免访问 null 或非对象类型时出错
   */
  const getPropertySafely = <T extends Record<string, any>, K extends keyof T>(obj: T, key: K): T[K] | undefined => {
    if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
      return undefined;
    }
    return obj[key];
  };

  /**
   * 解析单个列对象,转换为 property 和 label
   * @param column 待解析的列对象
   * @returns 若合法则返回 { property, label },否则返回 null
   */
  const parseColumnToPropertyLabel = (column: any): { property: string; label: string } | null => {
    if (typeof column !== "object" || column === null) {
      return null;
    }

    const property = getPropertySafely(column, "property");
    const label = getPropertySafely(column, "label");

    if (typeof property !== "string" || property.trim() === "") {
      return null;
    }

    const normalizedLabel = label == null ? "" : String(label).trim();

    return {
      property: property.trim(),
      label: normalizedLabel
    };
  };

  /**
   * 将列信息提取为 Record 结构
   * 该函数接收一个列数组,每个列包含 property 和 label 属性,并将其转换为一个键值对对象
   * 注意:遇到重复 property 时,仅保留第一个出现的项
   * @param columns 列数组,包含每个列的 property 和 label 属性
   * @returns 返回一个 Record 对象,其中键是列属性,值是列标签
   */
  const extractColumnsToRecord = (columns: Array<{ property: string; label: string }>): Record<string, string> => {
    if (!Array.isArray(columns) || columns.length === 0) {
      return {};
    }

    const resultMap: Record<string, string> = {};

    for (const column of columns) {
      const parsed = parseColumnToPropertyLabel(column);
      if (!parsed) continue;

      const { property, label } = parsed;

      // 若有重复的 property,保留第一个出现的项
      if (!(property in resultMap)) {
        resultMap[property] = label;
      }
    }

    return resultMap;
  };

  /**
   * 更新列属性和列标签的键值对
   * 该函数通过访问表格实例的列信息,过滤出具有 property 属性的列,并调用 extractColumnsToRecord 函数
   * 将其转换为键值对对象,然后将其赋值给 columnPropertyToLabelMap
   */
  const updateColumnPropertyLabelMap = () => {
    const columns = getTableColumns();
    if (!columns) {
      columnPropertyLabelMap.value = {};
      return;
    }

    columnPropertyLabelMap.value = extractColumnsToRecord(columns);
  };

  /**
   * 表格数据导出为 Excel 文件
   * @param filename 文件名
   */
  const tableDataExportToExcelFile = (filename?: string) => {
    const tableData = tableRef.value?.data ?? [];
    if (Object.keys(columnPropertyLabelMap.value).length === 0) {
      updateColumnPropertyLabelMap();
    }
    exportExcelFile(tableData, columnPropertyLabelMap.value, filename);
  };

  return {
    tableDataExportToExcelFile
  };
};

定义 ElTable 扩展实例类型 

/interface/index.ts

import { ElTable } from "element-plus";

// ElTable 扩展实例类型,包含内部 store 属性
export type ElTableExtendedInstance = InstanceType<typeof ElTable> & {
  store: {
    states: {
      columns: {
        property?: string;
        // 其他列属性...
      }[];
    };
  };
};

导出工具 

/utils/excelUtils.ts

import { formatDateTimeToYYYYMMDDHHMMSS } from "@/utils/pubUtils";
import * as xlsx from "xlsx";

/**
 * 导出Excel文件
 * @param dataList 数据列表
 * @param keyColMap 键值列名映射,key --> value,如:键值为【sampleNo】,其excel列名为【样品编号】
 * @param fileName 导出文件名
 */
export function exportExcelFile(dataList: any[], keyColMap: Record<string, string>, fileName?: string) {
  // 数据格式化,通过 keyColMap,如:将【sampleNo】映射为【样品编号】
  // 格式化前的数据样式:{id: 1, sampleNo: '25GD0001',  sampleName: '出厂水'}
  // keyColMap的数据样式:{id: '编号', sampleNo: '样品编号', sampleName: '样品名称'}
  // 格式化后的数据样式:{编号: 1, 样品编号: '25GD0001',  样品名称: '出厂水'}
  let data = dataList.map((item) => {
    let newItem: Record<string, any> = {};
    Object.entries(item).forEach(([key, value]) => {
      if (keyColMap[key] !== undefined) {
        newItem[keyColMap[key]] = value;
      }
    });
    return newItem;
  });

  // 导出空表,只有表头信息,用作模板导出,数据样式:[{编号: ''}, {样品编号: ''}, {样品名称: ''}]
  if (data.length === 0) {
    Object.entries(keyColMap).forEach(([key, value]) => {
      data.push({ [value]: "" });
    });
  }

  // 将 json 数据生成 sheet 表格数据
  let jsonSheet = xlsx.utils.json_to_sheet(data);

  // 构造工作薄对象 workbook
  let sheetName = fileName ? fileName : "info";
  let workBook = {
    SheetNames: [sheetName],
    Sheets: {
      [sheetName]: jsonSheet
    }
  };

  // 写出文件,将工作薄的内容生成文件并下载
  xlsx.writeFile(workBook, (fileName ? fileName : "info" + formatDateTimeToYYYYMMDDHHMMSS()) + ".xlsx");
}

 在组件中使用 

ReagentTransactionsDrawer.vue

<script setup lang="ts" name="ReagentTransactionsDrawer">

import { useElTableExtendedInstance } from "@/hooks/useElTableExtendedInstance";
import type { ElTableExtendedInstance } from "@/interface";

// 表格实例对象
const tableRef = ref<ElTableExtendedInstance | null>(null);
const { tableDataExportToExcelFile } = useElTableExtendedInstance(tableRef);

// 导出
const onExportClick = () => {
  // 表格数据导出为 Excel 文件
  tableDataExportToExcelFile("流转记录");
};

</script>

<template>

              <BasePreventReClickButton class="btn-same-width" type="primary" plain @click="onExportClick"
                >导出</BasePreventReClickButton
              >
          <el-table
            ref="tableRef"
            :data="transactionsList"
            :border="false"
            highlight-current-row
            stripe
            style="width: 100%; height: 100%">
            <el-table-column type="index" label="序号" width="60" fixed="left" header-align="center" align="center" />
            <el-table-column
              prop="materialCode"
              label="编号"
              width="120"
              fixed="left"
              header-align="left"
              show-overflow-tooltip />
            <el-table-column
              prop="materialName"
              label="名称"
              min-width="200"
              fixed="left"
              header-align="left"
              show-overflow-tooltip />
            <el-table-column
              prop="transactionTime"
              label="流转时间"
              width="165"
              header-align="left"
              sortable
              show-overflow-tooltip />
            <el-table-column
              prop="transactionType"
              label="类型"
              width="60"
              header-align="center"
              align="center"
              show-overflow-tooltip>
              <template #default="scope">
                <span>{{ scope.row.transactionType === 1 ? `入` : scope.row.transactionType === 2 ? `出` : `` }}</span>
              </template>
            </el-table-column>
            <el-table-column
              prop="amount"
              label="数量"
              width="80"
              header-align="center"
              align="center"
              show-overflow-tooltip>
              <template #default="{ row }">
                <span v-if="row.transactionType === 1" class="category-amount-in">+{{ row.amount }}</span>
                <span v-else-if="row.transactionType === 2" class="category-amount-out">-{{ row.amount }}</span>
              </template>
            </el-table-column>
            <el-table-column
              prop="total"
              label="金额"
              width="120"
              header-align="right"
              align="right"
              show-overflow-tooltip>
              <template #default="scope">
                {{ formatMoney(scope.row.total, "¥", 2) }}
              </template>
            </el-table-column>
            <el-table-column
              prop="orderNo"
              label="单据编号"
              width="120"
              header-align="center"
              align="center"
              show-overflow-tooltip />
          </el-table>

</template>

应用效果

所见所得的导出效果,除了【序号】没有导出,其余列都按顺序导出

同时支持空数据导出

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值