Skip to content

Excel 导入原理与实现

安装

bash
 npm i xlsx@0.17.0 -S

业务原理

注意

  1. 点击 excel 导入 按钮进入 excel 导入页面
  2. 页面提供了两种导入形式
    1. 点击按钮上传 excel
    2. excel 拖入指定区域
  3. 选中文件,进行两步操作
    1. 解析 excel 数据
    2. 上传解析之后的数据
  4. 上传成功之后,返回 员工管理(用户列表)页面,进行数据展示

所以根据这个业务我们可以看出,整个 excel 导入核心的原理部分在于 选中文件之后,上传成功之前 的操作,即:

  1. 解析 excel 数据(最重要
  2. 上传解析之后的数据
  • 根据上面所说,整个的实现流程我们也可以很轻松得出:
  1. 创建 excel 导入页面
  2. 点击 excel 导入按钮,进入该页面
  3. 该页面提供两种文件导入形式
  4. 选中文件之后,解析 excel 数据(核心)
  5. 上传解析之后的数据
  6. 返回 员工管理(用户列表) 页面

1. 创建 excel 导入页面

  • src/pages/import 页面

创建导入组件

  • src/components/UploadExcel.vue

  • en.js

bash
  uploadExcel: {
            upload: 'upload',
            drop: 'drop'
        }
  • cn.js
bash
  uploadExcel: {
            upload: '上传',
            drop: '拖拽'
        }

封装 excel.js

  • src/utils/excel.js
js
import XLSX from "xlsx";
/**
 * 获取表头(通用方式)
 */
export const getHeaderRow = (sheet) => {
  const headers = [];
  const range = XLSX.utils.decode_range(sheet["!ref"]);
  let C;
  const R = range.s.r;
  /* start in the first row */
  for (C = range.s.c; C <= range.e.c; ++C) {
    /* walk every column in the range */
    const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })];
    /* find the cell in the first row */
    let hdr = "UNKNOWN " + C; // <-- replace with your desired default
    if (cell && cell.t) hdr = XLSX.utils.format_cell(cell);
    headers.push(hdr);
  }
  return headers;
};

export const isExcel = (file) => {
  return /\.(xlsx|xls|csv)$/.test(file.name);
};

封装 hooks

  • src/hooks/admin/useButtonExcel.js
js
/**
 * @Author: jsopy
 * @Date: 2025-01-19 17:05:32
 * @LastEditTime: 2025-01-19 17:11:37
 * @FilePath: /adminvue/src/hooks/admin/useButtonExcel.js
 * @Description:
 * @
 */

import XLSX from "xlsx";

import { getHeaderRow } from "@/utils/excel";

export const useButtonExcel = (props) => {
  /**
   * 点击上传触发
   */
  const loading = ref(false);
  const excelUploadInput = ref(null);
  const handleUpload = () => {
    excelUploadInput.value.click();
  };
  const handleChange = (e) => {
    const files = e.target.files;
    const rawFile = files[0]; // only use files[0]
    if (!rawFile) return;
    upload(rawFile);
  };

  /**
   * 触发上传事件
   */
  const upload = (rawFile) => {
    excelUploadInput.value.value = null;
    // 如果没有指定上传前回调的话
    if (!props.beforeUpload) {
      readerData(rawFile);
      return;
    }
    // 如果指定了上传前回调,那么只有返回 true 才会执行后续操作
    const before = props.beforeUpload(rawFile);
    if (before) {
      readerData(rawFile);
    }
  };

  /**
   * 读取数据(异步)
   */
  const readerData = (rawFile) => {
    loading.value = true;
    return new Promise((resolve, reject) => {
      // https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader
      const reader = new FileReader();
      // 该事件在读取操作完成时触发
      // https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader/onload
      reader.onload = (e) => {
        // 1. 获取解析到的数据
        const data = e.target.result;
        // 2. 利用 XLSX 对数据进行解析
        const workbook = XLSX.read(data, { type: "array" });
        // 3. 获取第一张表格(工作簿)名称
        const firstSheetName = workbook.SheetNames[0];
        // 4. 只读取 Sheet1(第一张表格)的数据
        const worksheet = workbook.Sheets[firstSheetName];
        // 5. 解析数据表头
        const header = getHeaderRow(worksheet);
        // 6. 解析数据体
        const results = XLSX.utils.sheet_to_json(worksheet);
        // 7. 传入解析之后的数据
        generateData({ header, results });
        // 8. loading 处理
        loading.value = false;
        // 9. 异步完成
        resolve();
      };
      // 启动读取指定的 Blob 或 File 内容
      reader.readAsArrayBuffer(rawFile);
    });
  };

  /**
   * 根据导入内容,生成数据
   */
  const generateData = (excelData) => {
    props.onSuccess && props.onSuccess(excelData);
  };

  return {
    loading,
    excelUploadInput,
    handleUpload,
    handleChange,
    upload,
    readerData,
    generateData,
  };
};
  • src/hooks/admin/useDrag.js
js
import { getHeaderRow, isExcel } from "@/utils/excel";

import { ElMessage } from "element-plus";

export const useDrag = (loading, upload) => {
  /**
   * 拖拽文本释放时触发
   */
  const handleDrop = (e) => {
    // 上传中跳过
    if (loading.value) return;
    const files = e.dataTransfer.files;
    if (files.length !== 1) {
      ElMessage.error("必须要有一个文件");
      return;
    }
    const rawFile = files[0];
    if (!isExcel(rawFile)) {
      ElMessage.error("文件必须是 .xlsx, .xls, .csv 格式");
      return false;
    }
    // 触发上传事件
    upload(rawFile);
  };

  /**
   * 拖拽悬停时触发
   */
  const handleDragover = (e) => {
    // https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer/dropEffect
    // 在新位置生成源项的副本
    e.dataTransfer.dropEffect = "copy";
  };

  return {
    handleDrop,
    handleDragover,
  };
};

封装组件

vue
<template>
  <div class="upload-excel">
    <div class="btn-upload">
      <el-button
        :loading="loading"
        type="primary"
        @click="handleUpload"
        style="padding-left: 15px; padding-right: 15px"
      >
        {{ $t("message.uploadExcel.upload") }}
      </el-button>
    </div>

    <input
      ref="excelUploadInput"
      class="excel-upload-input"
      type="file"
      accept=".xlsx, .xls"
      @change="handleChange"
    />
    <!-- https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API -->
    <div
      class="drop"
      @drop.stop.prevent="handleDrop"
      @dragover.stop.prevent="handleDragover"
      @dragenter.stop.prevent="handleDragover"
    >
      <el-icon size="24" color="black" style="margin-left: 160px">
        <Upload />
      </el-icon>

      <span>{{ $t("message.uploadExcel.drop") }}</span>
    </div>
  </div>
</template>

<script setup>
import { useButtonExcel } from "@/hooks/admin/useButtonExcel";
import { useDrag } from "@/hooks/admin/useDrag.js";
const props = defineProps({
  // 上传前回调
  beforeUpload: Function,
  // 成功回调
  onSuccess: Function,
});
const {
  loading,
  excelUploadInput,
  handleUpload,
  handleChange,
  upload,
  readerData,
  generateData,
} = useButtonExcel(props);

const { handleDrop, handleDragover } = useDrag(loading, upload);
</script>

<style lang="scss" scoped>
.upload-excel {
  display: flex;
  justify-content: center;
  margin-top: 100px;
  .excel-upload-input {
    display: none;
    z-index: -9999;
  }
  .btn-upload,
  .drop {
    border: 1px dashed #bbb;
    width: 350px;
    height: 160px;
    text-align: center;
    line-height: 160px;
  }
  .drop {
    line-height: 60px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    color: #bbb;
    i {
      font-size: 60px;
      display: block;
    }
  }
}
</style>

调用

vue
<template>
  <div>
    <UploadExcel :onSuccess="onSuccess"></UploadExcel>
  </div>
</template>

<script setup>
/**
 * 数据解析成功之后的回调
 */
const onSuccess = (excelData) => {
  console.log(excelData);
};
</script>

<style lang="scss" scoped></style>