需求背景:项目里的附件上传以往都是通过调用后端上传附件接口,由后端接口负责校验附件以及表单规则,项目经理现为了优化性能,决定由前端先行校验表格内部分基础规则内容(如判断是否为空表格,列表项内容是否满足要求的格式,文件类型(.xls/.xlsx)等基础校验 Excel内容校验:通过xlsx库解析Excel文件,实现以下校验规则: 非空校验(N) 长度限制校验 数字文本校验(VI) 评分校验(VII,0-100保留1位小数) 整数校验(IS,1-100整数) 合并单元格处理等等,校验结果反馈:通过Message组件展示提示词),前端基础校验通过后再调用后端上传接口。
封装组件:
webUploadFile.vue
javascript
<template>
<div style="display: inline-flex">
<el-upload
class="upload-file"
ref="attachUpload"
accept="*"
:auto-upload="autoUpload"
:multiple="false"
:limit="1"
:action="uploadFileUrl"
:headers="headers"
:file-list="fileList"
:before-upload="beforeAvatarUpload"
:on-success="uploadSuccessHandle"
:on-error="uploadErrorHandle"
:data="fetchData"
>
<el-button size="mini" type="danger" icon="el-icon-upload2" :plain="plain">点击上传</el-button>
<!-- <span v-if="successVisible">未选择文件</span> -->
</el-upload>
</div>
</template>
<script>
import { getToken } from '@/utils/auth';
import uploadExcel from '@/utils/importValidate.js';
export default {
name: 'uploadFile',
props: {
// 上传文件接口地址
uploadFileUrl: {
type: String,
default: () => ''
},
// 从第几行开始校验数据
num: {
type: Number,
default: 1
},
// 校验规则数组
ruleArr: {
type: Array,
default: () => []
},
// 是否自动上传
autoUpload: {
type: Boolean,
default: false
},
// 是否补紧按钮
plain: {
type: Boolean,
default: false
},
fetchData: {
type: Object
},
sucessText: {
type: String,
default: () => '附件导入成功'
}
},
data() {
return {
fileAllowFiles: '.xls,.xlsx',
// uploadFileUrl: `${process.env.VUE_APP_BASE_API}/api/common/upload`
headers: {
Authorization: getToken()
},
successVisible: true,
fileListData: []
};
},
computed: {
// 商家名称的值
fileList: {
get() {
return this.fileListData;
},
set(val) {
this.$emit('update:fileListData', val);
}
}
},
methods: {
// 上传文件之前的钩子
beforeAvatarUpload(file, fileList) {
const size = file.size / 1024 / 1024 > 20;
let arr = file.name.split('.');
let fileType = arr[arr.length - 1].toLowerCase();
return new Promise((resolve, reject) => {
if (size) {
this.$message.warning('上传文件不能大于20M');
this.$emit('getResult', false);
return reject(false);
} else if (!this.fileAllowFiles.includes(fileType)) {
this.$message.warning(`不支持上传${fileType}类型的文件,请重传`);
this.$emit('getResult', false);
return reject(false);
}
uploadExcel(file, this.ruleArr, this.num).then((res) => {
if (!res) {
this.$emit('getResult', false);
return reject(false);
} else {
return resolve(true);
}
});
});
},
// 文件上传成功时的钩子
uploadSuccessHandle(response, file, fileList) {
if (response.code === 200) {
if (fileList.length > 0) {
this.$refs.attachUpload.uploadFiles = [];
this.$refs.attachUpload.uploadFiles.push(file);
this.successVisible = false;
}
// 在线签署管理== 同一个接口既需要文件上传又需要走公共的request code等于非200无法校验
if (response.data === null) {
this.$message.success(this.sucessText);
}
this.$emit('getResult', false, response);
this.$emit('uploadSuccess', this.$refs.attachUpload.uploadFiles);
} else {
this.$emit('getResult', false, response);
}
},
uploadErrorHandle(err) {
this.$message.error('服务器开个小差,请稍后再试');
this.$emit('getResult', false);
}
}
};
</script>
<style lang="less" scoped>
.upload-file {
.el-upload {
.el-button--default {
width: 100px;
height: 38px;
background: #ffffff !important;
font-family: MicrosoftYaHei;
font-size: 14px;
color: #9c9c9c;
font-weight: 400;
border: 1px solid rgba(221, 221, 221, 1);
}
/deep/ .el-upload-list__text {
min-width: 250px;
}
}
}
</style>
JavaScript 逻辑代码文件:
importValidate.js
javascript
import * as XLSX from "xlsx";
import { Message } from "element-ui";
// 调用各种数据类型的校验函数:
// "V": 类似varchar,可以任意字符,只需要校验长度
// "VM": 类似varchar,不能包含"'"及"&",需要校验长度
// "VA": 类似varchar,可以包括 /^[^'"\\()@$%^*<>&?]*$/,任意字符,只需要校验长度
// "VN": 类似varchar,任意字符但不可以包括/^[^<>\"]+$/,需要校验长度
// "C": 类似char,可以任意字符,只需要校验长度
// "N": 类似number(8.3)
// "I": 类似int
// "IS": 分数校验,只可以是1---100的整数
// "VI": 类似varchar,但只能是数字,要校验长度
// "VS": 类似varchar,但只能是数字,要校验长度,且只能输入1至7
// "VT": 类似varchar,但只能是数字,要校验长度,且只能输入1至10
// "VP": 类似varchar,但只能是数字,要校验长度,且只能输入大于1
// "CI": 类似char,但只能是数字,要校验长度
// "ID": 业务编号校验
// "D": 日期,8位,yyyymmdd,规则必须是"D::N"或"D::Y"
// "M": 邮件地址,类似varchar,但其中允许有"@"
// "LN": 检查输入仅限英文半角字符和数字,检查长度
// "CN": 检查输入仅限数字字符串,检查长度
// "EN": 检查输入仅限英文半角字符串,检查长度
function isDate1(str) {
// YYYYMM
if (String(str).length !== 6) {
return false;
}
let y = String(str).substring(0, 4);
let m = String(str).substring(4, 6) - 1;
let date = new Date(y, m);
if (date.getFullYear() === y && date.getMonth() === m) {
return true;
} else {
return false;
}
}
function isDate(str) {
// YYYYMMDD
if (String(str).length !== 8) {
return false;
}
let y = String(str).substring(0, 4);
let m = String(str).substring(4, 6) - 1;
let d = String(str).substring(6, 8);
let date = new Date(y, m, d);
if (date.getFullYear() === y && date.getMonth() === m && date.getDate() === d) {
return true;
} else {
return false;
}
}
function trim(str) {
let returnstr = "";
if (str === "") return "";
let i = 0;
for (i = 0; i < str.length; i++) {
if (str.charAt(i) === " ") {
continue;
}
break;
}
// str = "" + str;
str = str.substring(i, str.length);
if (str === "") return "";
for (i = str.length - 1; i >= 0; i--) {
if (str.charAt(i) === " ") {
continue;
}
break;
}
returnstr = str.substring(0, i + 1);
return returnstr;
}
function isDigit(theNum) {
let theMask = "0123456789";
if (isEmpty(theNum)) return false;
else if (theMask.indexOf(theNum) === -1) return false;
return true;
}
function isEmpty(e) {
let newString = trim(e);
if (newString === null || newString === "") return true;
else return false;
}
function isInt(theStr) {
let flag = true;
theStr = trim(theStr);
if (isEmpty(theStr)) flag = true;
else {
if (theStr.substring(0, 1) === "-") {
theStr = theStr.substring(1);
}
for (let i = 0; i < theStr.length; i++) {
if (isDigit(theStr.substring(i, i + 1)) === false) {
flag = false;
break;
}
}
}
return flag;
}
/**
* 处理合并单元格并填充数据
* @param {Object} worksheet - Excel工作表对象
* @returns {Object} 包含处理后的二维数组和合并区域信息
*/
function processMergedCells(worksheet) {
// 获取工作表的范围引用
const range = XLSX.utils.decode_range(worksheet["!ref"]);
// 创建二维数组保存数据
const data = [];
for (let R = range.s.r; R <= range.e.r; ++R) {
data.push([]);
for (let C = range.s.c; C <= range.e.c; ++C) {
data[R].push(null);
}
}
// 获取所有合并单元格区域
const merges = worksheet["!merges"] || [];
const mergeMap = {};
// 处理合并区域
merges.forEach((merge) => {
const start = merge.s; // 起始位置(左上角)
const end = merge.e; // 结束位置(右下角)
// 获取起始位置单元格的值
const address = XLSX.utils.encode_cell(start);
const cellValue = worksheet[address] ? worksheet[address].v : null;
// 将合并区域的所有单元格映射到起始单元格的值
for (let R = start.r; R <= end.r; ++R) {
for (let C = start.c; C <= end.c; ++C) {
// 标记单元格是否在合并区域中
mergeMap[`${R}:${C}`] = true;
// 左上角单元格标记
if (R === start.r && C === start.c) {
mergeMap[`${R}:${C}`] = "top-left";
}
// 填充值
data[R][C] = cellValue;
}
}
});
// 处理非合并单元格
Object.keys(worksheet).forEach((key) => {
if (key[0] === "!") return; // 跳过特殊键
const cell = worksheet[key];
const address = XLSX.utils.decode_cell(key);
// 只填充尚未处理过的单元格(非合并区域)
if (!mergeMap[`${address.r}:${address.c}`] && data[address.r][address.c] === null) {
data[address.r][address.c] = cell.v;
}
});
return { data, mergeMap };
}
function getfile(file, ruleData) {
let listTitle = [];
if (ruleData.length > 0) {
for (let i = 0; i < ruleData.length; i++) {
listTitle.push(ruleData[i].name);
}
} else {
alert("请定义校验规则!");
return false;
}
return new Promise(function (resolve, reject) {
const reader = new FileReader();
let result1 = [];
reader.onload = function (e) {
let binary = "";
let bytes = new Uint8Array(reader.result);
let lenth = bytes.byteLength;
for (let i = 0; i < lenth; i++) {
binary += String.fromCharCode(bytes[i]);
}
let workbook = XLSX.read(binary, { type: "binary" });
const sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
const { data: sheetData, mergeMap } = processMergedCells(sheet);
const arr = { data: sheetData, mergeMap }.data;
arr.forEach((item) => {
const result = listTitle.reduce((obj, key, index) => {
obj[key] = item[index];
return obj;
}, {});
result1.push(result);
});
resolve(result1);
};
reader.readAsArrayBuffer(file);
});
}
export async function uploadExcel(file, ruleData, num) {
let result = true;
const res = await getfile(file, ruleData);
if (res.length === num) {
alert("导入的Excel中没有数据,请校验");
result = false;
return false;
}
let tooptip = [];
for (let i = num; i < res.length; i++) {
for (let y = 0; y < ruleData.length; y++) {
if (!res[i][ruleData[y].name] && ruleData[y].isnull === "N") {
tooptip.push("请检查第" + ruleData[y].name + "列第" + (i + 1) + "行有空数据请重新提交");
result = false;
}
if (res[i][ruleData[y].name] && String(res[i][ruleData[y].name]).length > ruleData[y].leng) {
tooptip.push(
ruleData[y].name +
"列第" +
(i + 1) +
"行数据不能超过" +
ruleData[y].leng +
"个字请重新提交",
);
result = false;
}
if (res[i][ruleData[y].name] && ruleData[y].rule === "VI") {
let reg = /^[1-9]\d*$/;
if (!reg.test(+res[i][ruleData[y].name])) {
tooptip.push(ruleData[y].name + "列第" + (i + 1) + "行数据不是数字文本请重新提交");
result = false;
}
}
// 校验评分最大输入100且保留一位小数
if (res[i][ruleData[y].name] && ruleData[y].rule === "VII") {
let reg = /^(([1-9]\d*)|(0{1}))(\.\d{1})?$/;
if (!reg.test(res[i][ruleData[y].name])) {
tooptip.push(
ruleData[y].name +
"列第" +
(i + 1) +
"行请输入0-100的数字,最多保留一位小数,请重新提交",
);
result = false;
}
if (Number(res[i][ruleData[y].name]) !== 0 && !Number(res[i][ruleData[y].name])) {
tooptip.push(
ruleData[y].name +
"列第" +
(i + 1) +
"行请输入0-100的数字,最多保留一位小数,请重新提交",
);
result = false;
}
if (res[i][ruleData[y].name] > 100) {
tooptip.push(
ruleData[y].name +
"列第" +
(i + 1) +
"行请输入0-100的数字,最多保留一位小数,请重新提交",
);
result = false;
}
}
if (res[i][ruleData[y].name] && ruleData[y].rule === "IS") {
let reg = /^([0-9]|[1-9][0-9]|100)$/;
if (!reg.test(res[i][ruleData[y].name])) {
tooptip.push(ruleData[y].name + "列第" + (i + 1) + "行请输入0-100的整数请重新提交");
result = false;
}
}
}
}
if (!result) {
let tooptipStr = tooptip.join("<br/>");
Message.warning({
dangerouslyUseHTMLString: true,
showClose: true,
message: tooptipStr,
});
result = false;
}
return result;
// 遍历行与列进行校验
// for (let i = num; i < str.length; i++) {
// for (let y = 0; y < arr.length; y++) {
// // 校验:非空(isnull === 'N' 时不能为空)
// if (!str[i][arr[y].name] && arr[y].isnull === "N") {
// tooptip.push("请检查第" + arr[y].name + "列第" + (i + 1) + "行有空数据请重新提交");
// result = false;
// }
// if (str[i][arr[y].name] === "" && arr[y].isnull === "N") {
// tooptip.push("请检查第" + arr[y].name + "列第" + (i + 1) + "行有空数据请重新提交");
// result = false;
// }
// if (!String(str[i][arr[y].name]) && arr[y].isnull === "N") {
// tooptip.push("请检查第" + arr[y].name + "列第" + (i + 1) + "行有空数据请重新提交");
// result = false;
// }
// // 校验:长度限制
// if (str[i][arr[y].name] && String(str[i][arr[y].name]).length > arr[y].leng) {
// tooptip.push(
// "第" +
// arr[y].name +
// "列第" +
// (i + 1) +
// "行数据不能超过" +
// arr[y].leng +
// "个字符请重新提交",
// );
// result = false;
// }
// // 规则 VI:数字文本校验
// let reg = new RegExp("[0-9+]");
// if (str[i][arr[y].name] && reg.test(String(str[i][arr[y].name])) && arr[y].rule === "VI") {
// tooptip.push("第" + arr[y].name + "列第" + (i + 1) + "行数据不是数字文本请重新提交");
// result = false;
// }
// if (str[i][arr[y].name] && arr[y].rule === "VF") {
// let str1 = str[i][arr[y].name];
// if (str1.length === 1) {
// if (!(str1 >= 0 && str1 < 6)) {
// tooptip.push("第" + arr[y].name + "列第" + (i + 1) + "行数据不正确");
// result = false;
// }
// }
// // 长度3的小数校验(格式 x.xx,整数位0-8,小数位0-5)
// if (str1.length === 3) {
// let tmp = String(str1.split("."));
// if (tmp.length === 2) {
// // 是小数
// if (tmp[0] && tmp[1]) {
// if (!(tmp[0] >= 0 && tmp[0] < 8 && tmp[1] <= 5)) {
// tooptip.push("第" + arr[y].name + "列第" + (i + 1) + "行数据不正确");
// result = false;
// }
// }
// }
// }
// }
// if (str[i][arr[y].name] && arr[y].rule === "VII") {
// var reg = /^(([1-9]{1}\d*)|(0{1}))(\.\d{1})?$/;
// if (!reg.test(str[i][arr[y].name])) {
// tooptip.push(
// arr[y].name + "列第" + (i + 1) + "行请输入0-100的数字,最多保留一位小数,请重新提交",
// );
// result = false;
// }
// if (Number(str[i][arr[y].name]) !== 0 && !Number(str[i][arr[y].name])) {
// tooptip.push(
// arr[y].name + "列第" + (i + 1) + "行请输入0-100的数字,最多保留一位小数,请重新提交",
// );
// result = false;
// }
// if (str[i][arr[y].name] > 100) {
// tooptip.push(
// arr[y].name + "列第" + (i + 1) + "行请输入0-100的数字,最多保留一位小数,请重新提交",
// );
// result = false;
// }
// if (str[i][arr[y].name] < 0) {
// tooptip.push(
// arr[y].name + "列第" + (i + 1) + "行请输入0-100的数字,最多保留一位小数,请重新提交",
// );
// result = false;
// }
// }
// var reg = /^([0-9][0-9]{0,1}|100)$/;
// if (str[i][arr[y].name] && !reg.test(str[i][arr[y].name]) && arr[y].rule === "IS") {
// tooptip.push(arr[y].name + "列第" + (i + 1) + "行请输入0~100的整数请重新提交");
// result = false;
// }
// if (str[i][arr[y].name] && !(str[i][arr[y].name] > 1) && arr[y].rule === "VP") {
// tooptip.push(arr[y].name + "列第" + (i + 1) + "行请输入大于1的整数请重新提交");
// result = false;
// }
// let CIreg = new RegExp("^[0-9]*$");
// if (
// (str[i][arr[y].name] && !CIreg.test(str[i][arr[y].name]) && arr[y].rule === "CI") ||
// (String(str[i][arr[y].name]).length > arr[y].leng && arr[y].rule === "CI")
// ) {
// tooptip.push(
// arr[y].name + "列第" + (i + 1) + "行不能超过" + arr[y].leng + "个字符的整数请重新提交",
// );
// result = false;
// }
// let VNreg = /[^<>\""]+$/;
// if (str[i][arr[y].name] && !VNreg.test(str[i][arr[y].name]) && arr[y].rule === "VN") {
// tooptip.push(arr[y].name + "列第" + (i + 1) + "行不能包含特殊字符");
// result = false;
// }
// if (str[i][arr[y].name] && arr[y].rule === "D") {
// if (!isDate(str[i][arr[y].name])) {
// tooptip.push(arr[y].name + "列第" + (i + 1) + "行数据请输入日期(yyyymmdd)格式");
// result = false;
// }
// }
// if (str[i][arr[y].name] && arr[y].rule === "D1") {
// if (!isDate1(str[i][arr[y].name])) {
// tooptip.push(arr[y].name + "列第" + (i + 1) + "行数据请输入日期(yyyymm)格式");
// result = false;
// }
// }
// if (str[i][arr[y].name] && arr[y].rule === "VE") {
// if (!(0 <= str[i][arr[y].name] && str[i][arr[y].name] <= 120)) {
// tooptip.push(arr[y].name + "列第" + (i + 1) + "行数据请输入0~120的整数");
// result = false;
// }
// }
// if (str[i][arr[y].name] && arr[y].rule === "I") {
// if (!isInt(str[i][arr[y].name])) {
// tooptip.push(arr[y].name + "列第" + (i + 1) + "行数据不正确");
// result = false;
// }
// }
// }
// }
}
大致如此,大家可自行运行尝试调整一下代码。