发票自动识别功能前端开发(多表单动态生成)

一.先上效果图(全部代码见文章尾部)

8844a28034cc43abbf8821b3530106d5.jpeg

二.整体思路 

上面部分是v-for遍历生成走马灯的item标签(el-carousel-item),el-form放在每个标签里面去动态生成。

底部是自己写的一个滚动块,滚动块index和上面走马灯item的index下标实时保持对应。

应用流程/业务逻辑如下:(ocr模块支持图片和多张发票的pdf文件扫描上传)

        新增:

       点击底部滑块+号选择图片/pdf上传  -> 读取后端返回的数组对象,遍历并添加到el-carousel-item里面,移动滑块index到最新(上面的图片如果是照片就回显,如果是pdf文件,就显示一个默认logo,点击图片/pdf都打开新页面显示出来)  -> 编辑好信息后点击确定 -> 验证是否有重复,有重复阻止提交,并提示出重复的发票。

        编辑:

        编辑我这里业务相对简单,编辑的时候回显表单数据,隐藏底部滑块(意味着+号的新增也去掉了),发票号码,代码不可编辑,只可以对部分基础数据进行编辑。

 三.入参

 oriForm:表单数据,接收为数组,el-form根据这个值生成多组form表单

opeType:是新增还是编辑  参数为add/edit

props: {
    oriForm: {
      type: Array,
      default: [],
    },
    opeType: {
      type: String,
      default: "",
    },
  },

四.关键难点/坑点代码 

<el-form
     :ref="'dataForm' + mainIndex"        这里ref动态赋予
>
<el-upload
    :ref="'elUpload' + mainIndex"    上传文件的ref动态赋予
>
this.$set(this.model[active], "statementNo", this.settlementNo);    //form表单给值得时候用this.$set

this.$forceUpdate(); //有时候页面无法刷新,用这个方法

五.整体代码

<template>
  <div
    class="invoice-form"
    v-loading="pageLoading"
    element-loading-text="发票识别中..."
  >
    <el-carousel
      indicator-position="none"
      :autoplay="false"
      ref="elCarousel"
      :loop="false"
      height="490px"
      @change="changeCarousel"
    >
      <el-carousel-item
        :name="mainIndex.toString()"
        v-for="(mainItem, mainIndex) in model"
        :key="mainIndex"
      >
        <el-form
          :model="mainItem"
          :ref="'dataForm' + mainIndex"
          :rules="formRule"
        >
          <div class="invoice-img middle-center">
            <i
              v-if="opeType != 'edit'"
              class="el-icon-delete upload-delete-cl"
              @click.stop="deleteImg(mainIndex)"
            ></i>
            <el-upload
              action="/personal/ocr/invoice"
              :ref="'elUpload' + mainIndex"
              :disabled="opeType == 'edit'"
              drag
              :limit="1"
              :show-file-list="false"
              :before-upload="beforeUpload"
              :on-exceed="onExceed"
              :on-error="onError"
              :on-success="
                (row, info, c) => {
                  return onSuccess(row, info, c, mainIndex);
                }
              "
              class="el-upload-block"
            >
              <div class="top-img-box">
                <div
                  class="full-box"
                  @click.stop="openPdf(mainItem.invoiceImage)"
                  v-if="mainItem.contentType == 'application/pdf'"
                >
                  <div class="block-one middle-center">
                    <img src="../imgs/pdf4.jpg" class="pdf-img-cl" />
                  </div>

                  <div class="pdf-name-cl">{{ mainItem.fileName }}</div>
                </div>

                <div v-else class="full-box middle-center">
                  <div
                    class="upload-img-box middle-center"
                    v-if="mainItem.invoiceImage"
                  >
                    <img
                      class="top-img-cl"
                      :src="$baseUrl + mainItem.invoiceImage"
                    />
                  </div>

                  <div v-else>
                    <i class="el-icon-upload"></i>
                    <div class="el-upload__text">
                      将发票文件拖到此处自动识别,或<em>点击上传</em>
                    </div>
                  </div>
                </div>
              </div>
            </el-upload>
          </div>
          <el-row type="flex">
            <el-form-item
              class="item-cl"
              :label-width="labelWidth"
              label="对账单编号"
              prop="statementNo"
            >
              <select-remote
                :ref="'statementSelectRemote' + mainIndex"
                class="input-cl"
                :disabled="opeType == 'edit'"
                keyName="settlementNo"
                labelName="settlementNo"
                size="small"
                valueName="settlementNo"
                remoteUrl="/personal/credent/reconciliation/list"
                @change="(row) => changeStatement(row, mainIndex)"
                placeholder="支持对账单编号模糊查询"
                queryField="settlementNo"
                :pagination="true"
                :resultLine="['data', 'list']"
              ></select-remote>
            </el-form-item>
            <el-form-item
              class="item-cl"
              :label-width="labelWidth"
              label="发票号码"
              prop="invoiceNo"
            >
              <el-input
                clearable
                :disabled="opeType == 'edit'"
                placeholder="请输入发票号码"
                class="input-cl"
                size="small"
                v-model.trim="mainItem.invoiceNo"
              />
            </el-form-item>
          </el-row>
          <el-row type="flex">
            <el-form-item
              class="item-cl"
              :label-width="labelWidth"
              label="发票代码"
              prop="invoiceCode"
            >
              <el-input
                clearable
                :disabled="opeType == 'edit'"
                placeholder="请输入发票代码"
                class="input-cl"
                size="small"
                v-model.trim="mainItem.invoiceCode"
              />
            </el-form-item>
            <el-form-item
              class="item-cl"
              :label-width="labelWidth"
              label="开票日期"
              prop="billingShow"
            >
              <el-date-picker
                :unlink-panels="true"
                size="small"
                v-model.trim="mainItem.billingShow"
                type="datetime"
                value-format="yyyy-MM-dd HH:mm:ss"
                placeholder="请选择开票日期"
                class="input-cl"
                :clearable="false"
              >
              </el-date-picker>
            </el-form-item>
          </el-row>

          <el-row type="flex">
            <el-form-item
              class="item-cl"
              :label-width="labelWidth"
              label="合计金额"
              prop="totalAmt"
            >
              <el-input-number
                size="small"
                class="input-cl"
                v-model="mainItem.totalAmt"
                controls-position="right"
                :precision="2"
                :min="0"
              ></el-input-number>
            </el-form-item>
            <el-form-item
              class="item-cl"
              :label-width="labelWidth"
              label="票面金额"
              prop="invoiceAmt"
            >
              <el-input-number
                size="small"
                class="input-cl"
                v-model="mainItem.invoiceAmt"
                controls-position="right"
                :precision="2"
                :min="0"
              ></el-input-number>
            </el-form-item>
          </el-row>
          <el-row type="flex">
            <el-form-item
              class="item-cl"
              :label-width="labelWidth"
              label="税额"
              prop="taxAmt"
            >
              <el-input-number
                size="small"
                class="input-cl"
                v-model="mainItem.taxAmt"
                controls-position="right"
                :min="0"
                :precision="2"
              ></el-input-number>
            </el-form-item>
          </el-row>
        </el-form>
      </el-carousel-item>
    </el-carousel>
    <!-- 底部滚动模块 -->
    <div class="img-scoll middle-center" v-if="opeType != 'edit'">
      <div class="left-point middle-center">
        <i class="el-icon-arrow-left arr-cl" @click="moveLeft"></i>
      </div>
      <div class="middle-block">
        <div
          v-for="(mainItem, mainIndex) in model"
          :class="{
            'img-block-one': true,
            'middle-center': true,
            'active-class': carouselIndex == mainIndex,
          }"
          :key="mainIndex"
          @click="clickImg(mainIndex)"
        >
          <div class="for-center">
            <div
              v-if="mainItem.contentType == 'application/pdf'"
              class="bottom-img-box middle-center"
            >
              <img style="width: 70%" src="../imgs/pdf4.jpg" />
            </div>
            <div v-else class="bottom-img-box middle-center">
              <img
                style="width: 100%"
                v-if="mainItem.invoiceImage"
                :src="$baseUrl + mainItem.invoiceImage"
              />
              <el-empty
                v-else
                :image-size="50"
                description="描述文字"
              ></el-empty>
            </div>
          </div>
        </div>
        <div class="img-block-one middle-center" @click="addNewInvoice">
          <div class="for-center middle-center">
            <i class="el-icon-plus add-invoice"></i>
          </div>
        </div>
      </div>
      <div class="right-point middle-center" @click="moveRight">
        <i class="el-icon-arrow-right arr-cl"> </i>
      </div>
    </div>
  </div>
</template>
    <script>
import { getInvoiceFormRules } from "../configFiles/invoiceFormRules.js";
import selectRemote from "@/components/select/selectRemote.vue";
import { changeTime, changeDate } from "@/utils/public.js";
import {
  addInvoice,
  fileDetail,
  updateInvoice,
  getSelectRegistList,
} from "@/api/personalApi.js";
export default {
  components: { selectRemote },
  props: {
    oriForm: {
      type: Array,
      default: [],
    },
    opeType: {
      type: String,
      default: "",
    },
  },
  watch: {
    oriForm: {
      handler(val) {
        this.model = JSON.parse(JSON.stringify(val));
        console.log(this.model);
      },
      immediate: true,
    },
  },
  created() {
    console.log("传入的数据为:", this.model);
    this.formRule = getInvoiceFormRules(); //规则加载
    this.$nextTick(() => {
      this.oriData(); //针对1个需要懒加载的select框进行处理
    });
    if (this.opeType == "edit") {
      this.loadFileInfo(); //加载文件信息
      this.formatTime();
    }
  },
  data() {
    return {
      labelWidth: "100px",
      model: [],
      pageLoading: false,
      carouselIndex: 0, //幻灯片到哪一张了
      settlementNo: "", //一组发票信息只有一个对账单编号,记录编号
    };
  },
  methods: {
    oriData() {
      //查询对账单数据
      let _params = {
        settlementNo: "",
        pageNum: 1,
        pageSize: 20,
      };
      if (this.opeType == "edit") {
        _params.settlementNo = this.model[0].statementNo; //对账单编号始终保持一致,任意取一个
        this.settlementNo = this.model[0].statementNo;
      }

      getSelectRegistList(_params).then((res) => {
        this.model.forEach((item, mainIndex) => {
          let _aimRef = `statementSelectRemote${mainIndex}`;
          if (this.opeType == "edit") {
            //编辑的情况
            this.$refs[_aimRef][0].loadFirstData(res, _params.settlementNo);
          } else {
            this.$refs[_aimRef][0].loadFirstData(res);
          }
        });
      });
    },

    changeCarousel(active, ori) {
      this.carouselIndex = active;

      //重新赋值一次最新的对账单编号

      this.$nextTick(() => {
        let _aimRef = `statementSelectRemote${active}`;
        this.$refs[_aimRef][0].initData(this.settlementNo);
        console.log("this.settlementN", this.settlementNo);
        this.$set(this.model[active], "statementNo", this.settlementNo);
        this.$forceUpdate();
      });
    },

    formatTime() {
      this.model.forEach((item, index) => {
        this.$set(
          this.model[index],
          "billingShow",
          changeDate(item.billingDate) + " " + changeTime(item.billingTime)
        );
      });
    },

    loadFileInfo() {
      this.setFileInfo(this.model[0].invoiceImage);
    },

    clickImg(active) {
      this.$refs.elCarousel.setActiveItem(active);
    },

    moveLeft() {
      if (this.carouselIndex > 0) {
        this.clickImg(this.carouselIndex - 1);
      }
    },

    addNewInvoice() {
      let _judge = this.judgeForm();
      if (_judge._canAdd) {
        this.model.push({
          billingShow: "",
        });
        this.$nextTick(() => {
          this.$refs.elCarousel.setActiveItem(this.model.length - 1);
        });
      } else {
        this.$message({
          message: _judge._errorMsg,
          type: "warning",
        });
      }
    },

    openPdf(fileNo) {
      window.open(this.$baseUrl + fileNo, "_blank");
    },

    confirm() {
      let _judge = this.confirmJudgeForm();
      if (_judge._canAdd) {
        console.log("即将提交的数为:", this.model);
        //对时间做一下处理
        this.dealTime();
        if (this.opeType == "edit") {
          updateInvoice(this.model[0]).then((res) => {
            if (res.code == 200) {
              this.goSuccess("更新成功!");
            } else {
              this.goError("更新失败");
            }
          });
        } else {
          addInvoice(this.model).then((res) => {
            if (res.code == 200) {
              this.goSuccess("新增成功!");
            } else {
              this.goError("新增失败");
            }
          });
        }
      } else {
        this.$message({
          message: _judge._errorMsg,
          type: "warning",
        });
      }
    },

    dealTime() {
      this.model.forEach((item) => {
        item.billingDate = item.billingShow.split(" ")[0].replaceAll("-", "");
        item.billingTime = item.billingShow.split(" ")[1].replaceAll(":", "");
        // delete item.billingShow;
      });
    },

    goSuccess(msg) {
      this.$emit("btnConfirm");
      this.$message({
        message: msg,
        type: "success",
      });
    },

    goError(msg) {
      this.$message({
        message: msg,
        type: "error",
      });
    },

    confirmJudgeForm() {
      let fiJudge = this.judgeForm();
      if (!fiJudge._canAdd) {
        return fiJudge;
      }

      //继续对重复数据的校验
      let fKey,
        sKey,
        countArr = [],
        _canAdd = true,
        _errorMsg = "";
      this.model.forEach((item, index) => {
        countArr[index] = 0;
      });
      this.model.forEach((fItem, fIndex) => {
        fKey = fItem.invoiceNo + "-" + fItem.invoiceCode;
        this.model.forEach((sItem, sIndex) => {
          sKey = sItem.invoiceNo + "-" + sItem.invoiceCode;
          if (fKey == sKey) {
            countArr[fIndex]++;
          }
        });
      });

      let _model = this.model,
        reArr = [],
        _msg = "";
      countArr.forEach((item, index) => {
        if (item > 1) {
          _canAdd = false;
          _msg =
            "发票号码:" +
            _model[index].invoiceNo +
            "," +
            "发票代码:" +
            _model[index].invoiceCode +
            "数据存在重复;" +
            " ";
          reArr.push(_msg);
        }
      });
      reArr = new Set(reArr);
      reArr = Array.from(reArr);
      _errorMsg = reArr.join("");
      return {
        _canAdd,
        _errorMsg,
      };
    },

    judgeForm() {
      let _canAdd = true,
        _errorMsg = "存在未填写数据";

      this.model.forEach((item, index) => {
        let _aim = `dataForm${index}`;
        this.$refs[_aim][0].validate((valid) => {
          if (!valid) {
            _canAdd = false;
            return false;
          }
        });

        //同时检查一下照片的invoiceImage是否存在
        if (!item.invoiceImage) {
          _canAdd = false;
          _errorMsg = "存在发票信息未上传";
        }
      });

      return {
        _canAdd,
        _errorMsg,
      };
    },

    moveRight() {
      if (this.carouselIndex < this.model.length - 1) {
        this.clickImg(this.carouselIndex + 1);
      }
    },

    beforeUpload(file) {
      console.log("file文件为:", file);
      if (
        file.type == "image/jpeg" ||
        file.type == "image/png" ||
        file.type == "application/pdf"
      ) {
        this.pageLoading = true;
        return true;
      } else {
        this.$message({
          message: "仅支持jpg,png,pdf格式文件",
          type: "error",
        });
        return false;
      }
    },

    changeStatement(row, mainIndex) {
      this.settlementNo = row.value;
      this.model.forEach((item, index) => {
        this.$set(this.model[index], "statementNo", row.value);
      });
    },

    deleteImg(mainIndex) {
      if (this.model.length == 1) {
        this.$message.warning("至少保留一张发票");
        return;
      }
      this.$confirm(`确认删除当前发票?`, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      }).then(() => {
        this.model.splice(mainIndex, 1);

        this.$nextTick(() => {
          this.$refs.elCarousel.setActiveItem(mainIndex - 1);
        });
        this.$forceUpdate();
      });
    },

    onExceed() {
      this.$message.error("请先删除当前文件再进行上传");
    },

    onError() {
      this.pageLoading = false;
    },

    onSuccess(row, info, c, mainIndex) {
      console.log("发票返回信息为:", info);
      let _result = info.response;
      this.pageLoading = false;
      if (_result.code != 200) {
        this.$message({
          message: _result.msg,
          type: "error",
        });
        let _aimRef = `elUpload${mainIndex}`;
        this.$refs[_aimRef][0].clearFiles();
        return;
      }

      _result.data.forEach((item, reIndex) => {
        this.changeOriForm(this.model.length - 1, item, reIndex);
      });

      this.$forceUpdate();

      //再新增一个操作,将光标移动到最后一个位置
      this.$nextTick(() => {
        this.$refs.elCarousel.setActiveItem(this.model.length - 1);
      });

      this.$message.success("发票识别成功");
    },

    changeOriForm(_index, item, reIndex) {
      let _taxAmt = item.totalTax ? item.totalTax : "";
      let _billingShow = item.billingDate
        ? this.changeTimeFormat(item.billingDate, item)
        : "";
      let _invoiceAmt = item.amountTax ? item.amountTax : "";
      let _invoiceImage = item.fileNo ? item.fileNo : "";
      let _invoiceCode = item.invoiceCode ? item.invoiceCode : "";
      let _invoiceNo = item.invoiceNumber ? item.invoiceNumber : "";
      let _totalAmt = item.totalAmount ? item.totalAmount : "";
      let _billingTime = ""; //开票时间是没有的

      if (reIndex == 0) {
        //第一张发票覆盖,后面的push
        this.$set(this.model[_index], "taxAmt", _taxAmt); //税额
        this.$set(this.model[_index], "billingShow", _billingShow); //开票日期
        this.$set(this.model[_index], "invoiceAmt", _invoiceAmt); //票面金额
        this.$set(this.model[_index], "invoiceImage", _invoiceImage); //回显文件编号
        this.$set(this.model[_index], "invoiceCode", _invoiceCode); //发票代码
        this.$set(this.model[_index], "invoiceNo", _invoiceNo); //发票号码
        this.$set(this.model[_index], "totalAmt", _totalAmt); //合计金额
        console.log(this.model);
      } else {
        let _obj = {
          taxAmt: _taxAmt,
          billingShow: _billingShow,
          invoiceAmt: _invoiceAmt,
          invoiceImage: _invoiceImage,
          invoiceCode: _invoiceCode,
          invoiceNo: _invoiceNo,
          totalAmt: _totalAmt,
        };
        this.model.push(_obj);
      }

      this.setFileInfo(_invoiceImage); //同时将文件的类型合名称设置进去
    },

    setFileInfo(_invoiceNo) {
      let _index = this.model.length - 1;
      fileDetail({ fileNo: _invoiceNo }).then((res) => {
        this.$set(this.model[_index], "fileName", res.data.fileName);
        this.$set(this.model[_index], "contentType", res.data.contentType);
      });
    },

    changeTimeFormat(time, mainItem) {
      if (mainItem.billingTime) {
        return time + " " + item.billingTime;
      } else {
        return time + " " + "00:00:00";
      }
    },
  },
};
</script>
<style lang="scss" scoped>
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
/* 滚动槽 */
::-webkit-scrollbar-track {
  -webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.3);
  border-radius: 10px;
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
  border-radius: 10px;
  background: rgba(0, 0, 0, 0.1);
  -webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.5);
}
.invoice-form {
  .full-box {
    width: 100%;
    height: 100%;
  }
  .invoice-img {
    width: 100%;

    .top-img-box {
      width: 100%;
      height: 100%;

      .block-one {
        width: 100%;
        margin-top: 20px;

        .pdf-img-cl {
          width: 160px;
          height: 160px;
        }
      }

      .pdf-name-cl {
        width: 100%;
      }
    }

    .upload-img-box {
      width: 100%;
      height: 100%;
      overflow: auto;

      .top-img-cl {
        width: 100%;
      }
    }

    .upload-delete-cl {
      position: absolute;
      right: 12%;
      cursor: pointer;
      top: 0px;
      font-size: 18px;
    }

    .el-upload-block {
      margin-bottom: 15px;
      width: 70%;
      height: 230px;

      /deep/.el-upload {
        width: 100%;

        .el-upload-dragger {
          width: 100%;
          display: flex;
          align-items: center;
          justify-content: center;
          height: 220px;
        }

        .el-upload-dragger .el-icon-upload {
          margin: auto;
          line-height: 80px;
        }
      }
    }
  }

  .img-scoll {
    width: 100%;
    height: 98px;
    display: flex;
    margin-top: 20px;

    .left-point {
      width: 25px;
      height: 100%;
      cursor: pointer;

      &:hover {
        background: #efefef;
      }
    }

    .middle-block {
      width: calc(100% - 72px);
      padding: 0px 6px;
      white-space: nowrap;
      overflow: auto;
      display: flex;

      /deep/.el-empty {
        padding: 0px;
        .el-empty__description {
          display: none;
        }
      }

      .img-block-one {
        width: 80px;
        display: inline-block;
        height: 80px;
        border: 1px solid lightgray;
        margin-right: 10px;
        cursor: pointer;

        .add-invoice {
          font-size: 25px;
        }

        .for-center {
          width: 100%;
          height: 100%;

          .bottom-img-box {
            width: 100%;
            height: 100%;
          }
        }
      }

      .active-class {
        border: 2px solid #3388fb;
        width: 79px;
        height: 79px;
      }
    }

    .right-point {
      width: 25px;
      height: 100%;
      cursor: pointer;

      &:hover {
        background: #efefef;
      }
    }

    .arr-cl {
      font-size: 30px;
      font-weight: bold;
      color: lightgray;
      cursor: pointer;
    }
  }

  .item-cl {
    width: 50%;

    .input-cl {
      width: 90%;

      /deep/.el-input {
        width: 100%;
      }
    }
  }
}
</style>
    

 select-remote组件是我封装的远程搜索下拉组件,见前面的文章

invoiceFormRules.js文件是规则文件,类似如下:

export function getInvoiceFormRules() {
    return {
        statementNo: [{
            required: true,
            message: "请选择对账单编号",
            trigger: "change",
        }],
    }
}

changTime,changeDate方法比较简单

export function changeDate(str) { //将yyyyMMdd转换为yyyy-MM-dd 
    if (!str) return '';
    str = str.substr(0, 4) +
        "-" +
        str.substr(4, 2) +
        "-" +
        str.substr(6, 2);
    return str;
}

export function changeTime(str) { //将hhmmss转换为hh:mm:ss
    if (!str) return '';
    str = str.substr(0, 2) +
        ":" +
        str.substr(2, 2) +
        ":" +
        str.substr(4, 2);
    return str;
}

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一路追求匠人精神

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

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

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

打赏作者

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

抵扣说明:

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

余额充值