前言
在 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("请按要求重新修改上传内容");
}
}
该方法用于校验上传的数据,如果数据无误,则设置 validateStatus
为 true
,并显示成功提示;否则显示错误提示。
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可以优化,生成的数据有点冗余,过于嵌套了。