当前位置: 首页 > article >正文

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,
        });
      }
    };
 */


http://www.kler.cn/a/585009.html

相关文章:

  • [微服务设计]3_如何构建服务
  • golang从入门到做牛马:第二十二篇-Go语言并发:多任务的“协同作战”
  • 详细解析 ListView_GetEditControl()
  • Linux入门 全面整理终端 Bash、Vim 基础命令速记
  • Xxl-Job学习笔记
  • Vue系统学习day01
  • 258.反转字符串中的单词
  • 【每日学点HarmonyOS Next知识】span问题、组件标识属性、属性动画回调、图文混排、相对布局问题
  • Linux 部署Java应用程序
  • OpenCV实现图像分割与无缝合并
  • FORTRAN语言的数据结构
  • GStreamer —— 2.17、Windows下Qt加载GStreamer库后运行 - “播放教程 5:色彩平衡“(附:完整源码)
  • FastJSON与Java序列化:数据处理与转换的关键技术
  • Python爬虫实战:基于 Scrapy 框架的腾讯视频数据采集研究
  • 『Rust』Rust运行环境搭建
  • Linux笔记之通配符和正则表达式的区别
  • cocos creator 3.8如何在代码中打印drawcall,fps
  • Matlab 灰度质心法提取条纹中心线
  • Git的详细使用方法
  • 基于stm32的视觉物流机器人