一.先上效果图(全部代码见文章尾部)
二.整体思路
上面部分是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;
}