el-upload二次封装带表格校验组件

需求背景:项目里的附件上传以往都是通过调用后端上传附件接口,由后端接口负责校验附件以及表单规则,项目经理现为了优化性能,决定由前端先行校验表格内部分基础规则内容(如判断是否为空表格,列表项内容是否满足要求的格式,文件类型(.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;
  //         }
  //       }
  //     }
  //   }
}

大致如此,大家可自行运行尝试调整一下代码。

相关推荐
bearpping10 小时前
Nginx 配置:alias 和 root 的区别
前端·javascript·nginx
@大迁世界10 小时前
07.React 中的 createRoot 方法是什么?它具体如何运作?
前端·javascript·react.js·前端框架·ecmascript
颜酱12 小时前
DFS 岛屿系列题全解析
javascript·后端·算法
霍理迪12 小时前
Vue的响应式和生命周期
前端·javascript·vue.js
竹林81815 小时前
在Web3前端用Node.js子进程批量校验钱包,我踩了这些性能与安全的坑
javascript·node.js
Kel17 小时前
深入剖析 openai-node 源码:一个工业级 TypeScript SDK 的架构之美
javascript·人工智能·架构
SuperEugene18 小时前
Vue3 模板语法规范实战:v-if/v-for 不混用 + 表达式精简,避坑指南|Vue 组件与模板规范篇
开发语言·前端·javascript·vue.js·前端框架
Luna-player18 小时前
Vue 3 + Vue Router 的路由配置,简单示例
前端·javascript·vue.js