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>