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

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

相关文章:

  • 【AI】让deepseek_r1 671b输出draw.io可导入的xml图表数据
  • golang算法滑动窗口
  • 3.03-3.09 Web3 游戏周报:Sunflower Land 周留存率 74.2%,谁是本周最稳链游?
  • 数据库查问题常用OS命令汇总
  • mysql的MHA
  • 电商项目中如何选择安全高效的电商API接口?
  • 部署自己的Docker镜像加速仓库
  • Lineageos 22.1(Android 15)通知栏添加截图开关
  • 使用AI一步一步实现若依前端(4)
  • 【JAVA】之路启航——初识Java篇
  • python绘图之瀑布图
  • MySQL中有哪几种锁?
  • 个人学习编程(3-10) 刷题
  • 【C++】C++入门基础
  • SpringAI介绍及本地模型使用方法
  • c++介绍锁四
  • vim 编写/etc/docker/daemon.json文件时,E212: 无法打开并写入文件
  • 红队思想:Live off the Land - 靠山吃山,靠水吃水
  • 算法 之 树形dp 树的中心、重心
  • 深入理解序列并行化:sp_size 与批量大小参数详解
  • 取得金奖西瓜品种独家使用权的上海金山,为何要到异地“试种”?
  • 媒体评教师拎起学生威胁要扔下三楼:师风师德不能“悬空”
  • 蔡建忠已任昆山市副市长、市公安局局长
  • 浙江演艺集团7部作品组团来沪,今夏开启首届上海演出季
  • 终于,俄罗斯和乌克兰谈上了
  • 国家统计局公布2024年城镇单位就业人员年平均工资情况