【vue、UI】使用 Vue2 和 Element UI 封装 CSV 文件上传组件,实现csv回显

前言

在 Vue2 项目中,我们经常需要封装一些可重用的组件来提升开发效率。本文将介绍如何使用 Vue2 和 Element UI 封装一个用于上传 CSV 文件并在对话框中回显其内容的公共组件。此组件共涉及两个接口:一个用于校验 CSV 文件内容是否合规,另一个用于上传经过校验的 CSV 文件。

组件功能概述

该组件主要包括以下功能:

  • 选择 CSV 文件并上传。
  • 校验文件内容是否符合要求。
  • 将文件内容以表格形式展示。
  • 支持对不合规内容进行标记和提示。
  • 用户可在确认内容无误后手动点击上传。

实现效果

在这里插入图片描述

组件模板结构

首先来看组件的模板部分。

<template>
  <el-dialog
    :visible.sync="visible"
    title="上传 CSV 文件"
    width="50%"
    :before-close="handleClose"
    :close-on-click-modal="false"
  >
    <el-upload
      :action="!validateStatus ? validate : action"
      :before-upload="beforeUpload"
      :show-file-list="false"
      :headers="headers"
      ref="upload"
      :on-success="handleAvatarSuccess"
      :auto-upload="!validateStatus"
      :file-list="fileList"
      :on-error="errorFn"
    >
      <el-button type="primary" @click="selectFile">选择 CSV 文件</el-button>
    </el-upload>

    <el-table
      v-if="tableData.length > 0"
      :data="tableData.slice(1)"
      style="width: 100%; margin-top: 20px"
      border
    >
      <el-table-column
        v-for="(header, index) in tableData[0]"
        :key="'header-' + index"
        :prop="'col-' + index"
        :label="header"
      >
        <template slot-scope="scope">
          <div :class="{ 'error-cell': scope.row[index].value.isError }">
            {{ scope.row[index].value.value }}
            <el-tooltip
              class="item"
              effect="dark"
              placement="top"
              v-if="scope.row[index].value.isError"
            >
              <template slot="content">
                {{ scope.row[index].value.errorMsg }}</template
              >
              <i class="el-icon-question"></i>
            </el-tooltip>
          </div>
        </template>
      </el-table-column>
    </el-table>

    <span slot="footer" class="dialog-footer">
      <el-button @click="handleClose">关 闭</el-button>
      <el-button
        type="primary"
        @click="handleConfirm"
        :disabled="!validateStatus"
        :title="!validateStatus ? '请上传文件并通过校验' : '点击上传文件'"
        >确定上传</el-button
      >
    </span>
  </el-dialog>
</template>

在这个模板中,使用了 Element UI 的 el-dialog 作为弹出框,el-upload 作为上传组件,el-table 显示上传的 CSV 文件内容。组件的主要逻辑操作通过各种方法(methods)来实现。

组件的核心逻辑

1.数据属性定义

以下是组件的数据属性,用于存储上传文件的状态、表格数据和 HTTP 请求头信息

data() {
  return {
    tableData: [], // 存储解析后的表格数据
    headers: {
      Authorization: "Bearer " + getToken(),
    },
    validateStatus: false,
    fileList: [], // 存储上传的文件
  };
}
  • tableData:存储解析后的 CSV 文件数据。
  • headers:HTTP 请求头,包含授权信息。
  • validateStatus:文件校验状态,决定文件是否可以被上传。
  • fileList:用于存储选择的 CSV 文件。

2.方法拆解

selectFile() 方法

selectFile() {
  this.validateStatus = false;
}

当用户点击“选择 CSV 文件”按钮时,重置 validateStatus 状态为 false,确保在选择新文件时校验状态被重置

validateData(data) 方法

validateData(data) {
  const allData = Object.values(data).flat();

  // 检查是否所有的 isError 都为 false
  const allValid = allData.every((item) => !item.isError);

  if (allValid) {
    this.validateStatus = true;
    this.$message.success("校验通过,可以上传");
  } else {
    this.validateStatus = false;
    this.$message.error("请按要求重新修改上传内容");
  }
}

该方法用于校验上传的数据,如果数据无误,则设置 validateStatustrue,并显示成功提示;否则显示错误提示。

handleAvatarSuccess(res, file) 方法

handleAvatarSuccess(res, file) {
  if (res.code === 500) {
    this.$message.error(res.msg);
    this.validateStatus = false;
    return;
  }

  if (res.data) {
    const csvHeaders = this.tableData[0];

    const fields = Object.keys(res.data);
    const dataLength = res.data[fields[0]].length;

    const result = Array.from({ length: dataLength }, (_, index) =>
      fields.map((field) => ({
        value: res.data[field][index],
        isError: false,
        errorMsg: "",
      }))
    );

    this.tableData = [csvHeaders, ...result];
    this.validateData(res.data);
  }
}

该方法在文件上传成功后调用,处理上传成功后的逻辑,包括数据解析和更新表格数据。

handleClose() 方法

handleClose() {
  this.tableData = [];
  this.$emit("update:visible", false);
}

关闭对话框时,清空表格数据并触发 visible 属性更新事件,关闭对话框。

handleConfirm() 方法

handleConfirm() {
  const formData = new FormData();
  formData.append("file", this.fileList[0]);

  const config = {
    headers: this.headers,
  };

  axios
    .post(this.action, formData, config)
    .then((response) => {
      if (response.data.code == 500 && response.data.msg == null) {
        this.$message.error("文件提交失败,未知原因");
      } else if (response.data.code == 200) {
        this.$message({
          dangerouslyUseHTMLString: true,
          message: response.data.msg,
          type: "success",
          duration: 5000,
        });
        this.$emit("upload-success");
      }
    })
    .catch((error) => {
      this.$message.error("文件提交失败");
    });
}

该方法在用户确认上传时调用,通过 Axios 发起 POST 请求将文件上传至后端接口。

3.CSV 文件解析方法

beforeUpload(file) 方法

beforeUpload(file) {
  if (!this.validateStatus) {
    const reader = new FileReader();
    reader.onload = (e) => {
      const decoder = new TextDecoder("gbk");
      const csvText = decoder.decode(e.target.result);
      this.tableData = this.parseCSV(csvText);

      if (this.tableData.length > 0) {
        return true;
      } else {
        return false;
      }
    };
    reader.readAsArrayBuffer(file);
    this.fileList = [file];
  } else {
    return true;
  }
}

该方法在文件上传之前执行,使用 FileReader 对象解析 CSV 文件内容,并将其转换为表格数据。

parseCSV(text) 方法

parseCSV(text) {
  const lines = text.split("\n").map((line) => line.trim());
  if (lines.length === 0) return [];

  const headers = this.parseLine(lines[0]);
  const data = lines.slice(1).map((line) => {
    const cells = this.parseLine(line);
    const row = headers.map((header, index) => ({
      value: cells[index] || "",
      isError: false,
      errorMsg: "",
    }));
    return row;
  });

  return [headers, ...data];
}

该方法用于解析 CSV 文件内容,按行拆分并解析每一行内容为表格所需的格式。

4. 错误处理方法

errorFn(err, file, fileList) 方法

errorFn(err, file, fileList) {
  console.log("🚀 ~ errorFn ~ err:", err);
}

该方法处理文件上传过程中的错误,当前仅简单地打印错误信息。

组件样式

<style scoped>
.el-table th,
.el-table td {
  text-align: center;
  padding: 10px;
}

.error-cell {
  background-color: #ffb1b1;
  color: black;
  padding: 5px;
}
</style>

此部分为组件的样式定义,确保表格居中对齐,并为有错误的单元格添加红色背景。

完整组件代码

完整的组件代码如下所示。这段代码结合了 Vue2 和 Element UI,封装了一个 CSV 文件上传与显示的功能组件。

<template>
  <el-dialog
    :visible.sync="visible"
    title="上传 CSV 文件"
    width="50%"
    :before-close="handleClose"
    :close-on-click-modal="false"
  >
    <el-upload
      :action="!validateStatus ? validate : action"
      :before-upload="beforeUpload"
      :show-file-list="false"
      :headers="headers"
      ref="upload"
      :on-success="handleAvatarSuccess"
      :auto-upload="!validateStatus"
      :file-list="fileList"
      :on-error="errorFn"
    >
      <el-button type="primary" @click="selectFile">选择 CSV 文件</el-button>
    </el-upload>

    <el-table
      v-if="tableData.length > 0"
      :data="tableData.slice(1)"
      style="width: 100%; margin-top: 20px"
      border
    >
      <el-table-column
        v-for="(header, index) in tableData[0]"
        :key="'header-' + index"
        :prop="'col-' + index"
        :label="header"
      >
        <template slot-scope="scope">
          <div :class="{ 'error-cell': scope.row[index].value.isError }">
            {{ scope.row[index].value.value }}
            <el-tooltip
              class="item"
              effect="dark"
              placement="top"
              v-if="scope.row[index].value.isError"
            >
              <template slot="content">
                {{ scope.row[index].value.errorMsg }}</template
              >
              <i class="el-icon-question"></i>
            </el-tooltip>
          </div>
        </template>
      </el-table-column>
    </el-table>

    <span slot="footer" class="dialog-footer">
      <el-button @click="handleClose">关 闭</el-button>
      <el-button
        type="primary"
        @click="handleConfirm"
        :disabled="!validateStatus"
        :title="!validateStatus ? '请上传文件并通过校验' : '点击上传文件'"
        >确定上传</el-button
      >
    </span>
  </el-dialog>
</template>

<script>
import { getToken } from "@/utils/auth";
import axios from "axios";
export default {
  props: {
    visible: {
      type: Boolean,
      required: true,
    },
    action: {
      type: String,
      required: true,
    },
    validate: {
      type: String,
      required: true,
    },
  },
  data() {
    return {
      tableData: [], // 存储解析后的表格数据
      headers: {
        Authorization: "Bearer " + getToken(),
      },
      validateStatus: false,
      fileList: [], // 存储上传的文件
    };
  },
  methods: {
    selectFile() {
      this.validateStatus = false;
    },
    validateData(data) {
      const allData = Object.values(data).flat();

      const allValid = allData.every((item) => !item.isError);

      if (allValid) {
        this.validateStatus = true;
        this.$message.success("校验通过,可以上传");
      } else {
        this.validateStatus = false;
        this.$message.error("请按要求重新修改上传内容");
      }
    },
    handleAvatarSuccess(res, file) {
      if (res.code === 500) {
        this.$message.error(res.msg);
        this.validateStatus = false;
        return;
      }

      if (res.data) {
        const csvHeaders = this.tableData[0];

        const fields = Object.keys(res.data);
        const dataLength = res.data[fields[0]].length;

        const result = Array.from({ length: dataLength }, (_, index) =>
          fields.map((field) => ({
            value: res.data[field][index],
            isError: false,
            errorMsg: "",
          }))
        );

        this.tableData = [csvHeaders, ...result];
        this.validateData(res.data);
      }
    },

    handleClose() {
      this.tableData = [];
      this.$emit("update:visible", false);
    },
    handleConfirm() {
      const formData = new FormData();
      formData.append("file", this.fileList[0]);

      const config = {
        headers: this.headers,
      };

      axios
        .post(this.action, formData, config)
        .then((response) => {
          if (response.data.code == 500 && response.data.msg == null) {
            this.$message.error("文件提交失败,未知原因");
          } else if (response.data.code == 200) {
            this.$message({
              dangerouslyUseHTMLString: true,
              message: response.data.msg,
              type: "success",
              duration: 5000,
            });
            this.$emit("upload-success");
          }
        })
        .catch((error) => {
          this.$message.error("文件提交失败");
        });
    },
    beforeUpload(file) {
      if (!this.validateStatus) {
        const reader = new FileReader();
        reader.onload = (e) => {
          const decoder = new TextDecoder("gbk");
          const csvText = decoder.decode(e.target.result);
          this.tableData = this.parseCSV(csvText);

          if (this.tableData.length > 0) {
            return true;
          } else {
            return false;
          }
        };
        reader.readAsArrayBuffer(file);
        this.fileList = [file];
      } else {
        return true;
      }
    },
    errorFn(err, file, fileList) {
      console.log("🚀 ~ errorFn ~ err:", err);
    },
    parseCSV(text) {
      const lines = text.split("\n").map((line) => line.trim());
      if (lines.length === 0) return [];

      const headers = this.parseLine(lines[0]);
      const data = lines.slice(1).map((line) => {
        const cells = this.parseLine(line);
        const row = headers.map((header, index) => ({
          value: cells[index] || "",
          isError: false,
          errorMsg: "",
        }));
        return row;
      });

      return [headers, ...data];
    },

    parseLine(line) {
      const result = [];
      let current = "";
      let inQuotes = false;

      for (let i = 0; i < line.length; i++) {
        const char = line[i];

        if (char === '"') {
          inQuotes = !inQuotes;
        } else if (char === "," && !inQuotes) {
          result.push(current.trim());
          current = "";
        } else {
          current += char;
        }
      }

      result.push(current.trim());

      return result;
    },
  },
};
</script>

<style scoped>
.el-table th,
.el-table td {
  text-align: center;
  padding: 10px;
}

.error-cell {
  background-color: #ffb1b1;
  color: black;
  padding: 5px;
}
</style>

总结

通过本文的介绍,了解了如何使用 Vue2 和 Element UI 封装一个 CSV 文件上传和回显的组件。该组件的设计充分考虑了数据校验和用户体验,使得上传和展示过程更加直观和友好。在实际项目中,可以根据业务需求对该组件进行扩展和定制。希望这篇文章对您有所帮助!

待优化的地方

文件handleAvatarSuccess可以优化,生成的数据有点冗余,过于嵌套了。

  • 10
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fruge365

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值