<template>
|
<div class="app-container">
|
<el-form
|
class="opo-query-form"
|
:model="queryParams"
|
ref="queryForm"
|
:inline="true"
|
v-show="showSearch"
|
label-width="90px"
|
>
|
|
<el-form-item label="发票名称" prop="invoiceName">
|
<el-input
|
v-model="queryParams.invoiceName"
|
placeholder="请输入发票名称"
|
clearable
|
size="small"
|
@keyup.enter.native="handleQuery"
|
/>
|
</el-form-item>
|
<el-form-item label="案列号码" prop="caseId">
|
<el-input
|
v-model="queryParams.caseId"
|
placeholder="案列号码"
|
clearable
|
size="small"
|
@keyup.enter.native="handleQuery"
|
/>
|
</el-form-item>
|
<el-form-item label="报销状态" prop="reimburseState">
|
<el-select
|
v-model="queryParams.reimburseState"
|
placeholder="请选择报销状态"
|
clearable
|
size="small"
|
style="width: 140px"
|
>
|
<el-option label="未报销" value="0" />
|
<el-option label="已报销" value="1" />
|
</el-select>
|
</el-form-item>
|
<el-form-item>
|
<el-button
|
type="primary"
|
icon="el-icon-search"
|
size="mini"
|
@click="handleQuery"
|
>搜索</el-button
|
>
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery"
|
>重置</el-button
|
>
|
</el-form-item>
|
</el-form>
|
|
<el-row :gutter="10" class="mb8">
|
<el-col :span="1.5">
|
<el-button
|
type="primary"
|
plain
|
icon="el-icon-upload2"
|
size="mini"
|
@click="handleUpload"
|
v-hasPermi="['project:invoice:add']"
|
>上传发票</el-button
|
>
|
</el-col>
|
<right-toolbar
|
:showSearch.sync="showSearch"
|
@queryTable="getList"
|
></right-toolbar>
|
</el-row>
|
|
<el-table v-loading="loading" border :data="invoiceList">
|
<el-table-column type="index" label="序号" width="55" align="center" />
|
<el-table-column label="案例" align="center" prop="caseId" width="100">
|
|
</el-table-column>
|
|
<el-table-column
|
prop="invoiceType"
|
label="发票类型"
|
width="130"
|
align="center"
|
show-overflow-tooltip
|
/>
|
<el-table-column
|
prop="invoiceName"
|
label="发票名称"
|
align="center"
|
show-overflow-tooltip
|
/>
|
<el-table-column
|
prop="invoiceNo"
|
label="发票号码"
|
width="150"
|
align="center"
|
show-overflow-tooltip
|
/>
|
<el-table-column
|
prop="invoiceAmount"
|
label="发票金额"
|
width="120"
|
align="center"
|
/>
|
<el-table-column label="发生日期" width="120" align="center">
|
<template slot-scope="scope">
|
{{ normalizeDate(scope.row.occurDate) || scope.row.occurDate }}
|
</template>
|
</el-table-column>
|
<el-table-column label="报销状态" width="110" align="center">
|
<template slot-scope="scope">
|
<el-tag
|
size="mini"
|
:type="
|
String(scope.row.reimburseState) === '1' ? 'success' : 'info'
|
"
|
>
|
{{ String(scope.row.reimburseState) === "1" ? "已报销" : "未报销" }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column
|
label="操作"
|
width="200"
|
align="center"
|
fixed="right"
|
>
|
<template slot-scope="scope">
|
<el-button
|
type="text"
|
icon="el-icon-view"
|
@click="openPreview(scope.row.invoiceUrl)"
|
>文件预览</el-button
|
>
|
<el-button
|
v-if="String(scope.row.reimburseState) !== '1'"
|
type="text"
|
icon="el-icon-check"
|
@click="handleReimburse(scope.row)"
|
v-hasPermi="['project:invoice:edit']"
|
>已报销</el-button
|
>
|
<el-button
|
type="text"
|
icon="el-icon-edit"
|
@click="handleUpdate(scope.row)"
|
v-hasPermi="['project:invoice:edit']"
|
>修改</el-button
|
>
|
<el-button
|
type="text"
|
icon="el-icon-delete"
|
@click="handleDelete(scope.row)"
|
v-hasPermi="['project:invoice:remove']"
|
>删除</el-button
|
>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<pagination
|
v-show="total > 0"
|
:total="total"
|
:page.sync="queryParams.pageNum"
|
:limit.sync="queryParams.pageSize"
|
@pagination="getList"
|
/>
|
|
<!-- 上传发票 / 发票信息确认 -->
|
<el-dialog
|
:title="title"
|
:visible.sync="open"
|
width="760px"
|
append-to-body
|
:close-on-click-modal="false"
|
>
|
<!-- 第一步:选案例、用户,上传发票 -->
|
<div v-if="mode === 'upload'">
|
<el-form
|
ref="uploadForm"
|
:model="uploadForm"
|
:rules="uploadRules"
|
label-width="100px"
|
>
|
<el-row>
|
<el-col :span="12">
|
<el-form-item label="案例ID" prop="caseId">
|
<el-input
|
v-model.number="uploadForm.caseId"
|
placeholder="请输入案例ID"
|
/>
|
</el-form-item>
|
</el-col>
|
|
</el-row>
|
<el-form-item label="发票文件">
|
<el-upload
|
class="invoice-upload"
|
drag
|
multiple
|
:action="uploadFileUrl"
|
:headers="headers"
|
:data="uploadData"
|
:show-file-list="false"
|
:before-upload="beforeUpload"
|
:on-success="handleUploadSuccess"
|
:on-error="handleUploadError"
|
accept="image/*,.pdf"
|
>
|
<i class="el-icon-upload"></i>
|
<div class="el-upload__text">
|
将发票拖到此处,或<em>点击上传</em>
|
</div>
|
<div class="el-upload__tip" slot="tip">
|
支持图片、PDF,可多选文件批量上传,单个文件不超过 10MB;上传后自动识别发票信息,识别结果可在下方编辑
|
</div>
|
</el-upload>
|
</el-form-item>
|
<el-form-item
|
v-if="uploadedList.length"
|
label="上传结果"
|
>
|
<el-table
|
:data="uploadedList"
|
border
|
size="mini"
|
max-height="260"
|
style="width: 100%"
|
>
|
<el-table-column
|
prop="invoiceNo"
|
label="发票号码"
|
show-overflow-tooltip
|
/>
|
<el-table-column
|
prop="invoiceType"
|
label="发票类型"
|
show-overflow-tooltip
|
/>
|
<el-table-column
|
prop="invoiceAmount"
|
label="发票金额"
|
width="110"
|
align="center"
|
/>
|
<el-table-column label="发生日期" width="120" align="center">
|
<template slot-scope="scope">
|
{{
|
normalizeDate(scope.row.occurDate) || scope.row.occurDate
|
}}
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="80" align="center">
|
<template slot-scope="scope">
|
<el-button
|
type="text"
|
@click="openEditForm(scope.row)"
|
>编辑</el-button
|
>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-form-item>
|
</el-form>
|
</div>
|
|
<!-- 第二步:识别结果确认,字段均可修改 -->
|
<el-form
|
v-else
|
ref="form"
|
:model="form"
|
:rules="rules"
|
label-width="100px"
|
>
|
<el-row>
|
<el-col :span="12">
|
<el-form-item label="案例ID" prop="caseId">
|
<el-input
|
v-model.number="form.caseId"
|
placeholder="请输入案例ID"
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="发票类型" prop="invoiceType">
|
<el-input
|
v-model="form.invoiceType"
|
placeholder="请输入发票类型"
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="发票名称" prop="invoiceName">
|
<el-input
|
v-model="form.invoiceName"
|
placeholder="请输入发票名称"
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="发票号码" prop="invoiceNo">
|
<el-input
|
v-model="form.invoiceNo"
|
placeholder="请输入发票号码"
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="发票金额" prop="invoiceAmount">
|
<el-input
|
v-model="form.invoiceAmount"
|
placeholder="请输入发票金额"
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="发生日期" prop="occurDate">
|
<el-date-picker
|
v-model="form.occurDate"
|
type="date"
|
value-format="yyyy-MM-dd"
|
placeholder="请选择发生日期"
|
style="width: 100%"
|
/>
|
</el-form-item>
|
</el-col>
|
|
<el-col :span="12">
|
<el-form-item label="发票开始日期" prop="invoiceStartDate">
|
<el-date-picker
|
v-model="form.invoiceStartDate"
|
type="date"
|
value-format="yyyy-MM-dd"
|
placeholder="请选择发票开始日期"
|
style="width: 100%"
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="发票结束日期" prop="invoiceEndDate">
|
<el-date-picker
|
v-model="form.invoiceEndDate"
|
type="date"
|
value-format="yyyy-MM-dd"
|
placeholder="请选择发票结束日期"
|
style="width: 100%"
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="销售方名称" prop="sellerName">
|
<el-input
|
v-model="form.sellerName"
|
placeholder="请输入销售方名称"
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="报销状态" prop="reimburseState">
|
<el-select
|
v-model="form.reimburseState"
|
placeholder="请选择报销状态"
|
style="width: 100%"
|
>
|
<el-option label="未报销" value="0" />
|
<el-option label="已报销" value="1" />
|
</el-select>
|
</el-form-item>
|
</el-col>
|
<el-col :span="24">
|
<el-form-item label="备注" prop="remark">
|
<el-input
|
v-model="form.remark"
|
type="textarea"
|
:rows="2"
|
maxlength="500"
|
show-word-limit
|
placeholder="请输入备注"
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="24">
|
<el-form-item label="发票预览">
|
<el-button
|
type="text"
|
icon="el-icon-view"
|
:disabled="!form.invoiceUrl"
|
@click="openPreview(form.invoiceUrl)"
|
>文件预览</el-button
|
>
|
<el-image
|
v-if="
|
form.invoiceUrl && getFileType(form.invoiceUrl) === 'image'
|
"
|
:src="form.invoiceUrl"
|
:preview-src-list="[form.invoiceUrl]"
|
fit="contain"
|
class="invoice-thumb"
|
/>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
</el-form>
|
|
<span slot="footer" class="dialog-footer">
|
<el-button
|
v-if="mode === 'edit' && uploadedList.length > 1"
|
@click="mode = 'upload'"
|
>返回列表</el-button
|
>
|
<el-button @click="open = false">取 消</el-button>
|
<el-button
|
v-if="mode === 'edit'"
|
type="primary"
|
:loading="saving"
|
@click="submitForm"
|
>保 存</el-button
|
>
|
</span>
|
</el-dialog>
|
|
<!-- 发票文件预览 -->
|
<el-dialog
|
title="发票文件预览"
|
:visible.sync="previewOpen"
|
width="900px"
|
append-to-body
|
:destroy-on-close="true"
|
>
|
<div class="invoice-preview">
|
<img
|
v-if="previewType === 'image'"
|
:src="previewUrl"
|
class="preview-image"
|
/>
|
<iframe
|
v-else-if="previewType === 'pdf'"
|
:src="previewUrl"
|
class="preview-frame"
|
></iframe>
|
<div v-else class="preview-tip">
|
该文件类型不支持在线预览,请
|
<el-link type="primary" :href="previewUrl" target="_blank"
|
>在新窗口打开</el-link
|
>
|
</div>
|
</div>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script>
|
import {
|
listInvoice,
|
getInvoice,
|
updateInvoice,
|
delInvoice
|
} from "@/api/project/invoice";
|
import { getUserProfile } from "@/api/system/user";
|
import { getToken } from "@/utils/auth";
|
|
|
|
export default {
|
name: "ServiceInvoice",
|
data() {
|
return {
|
// 遮罩层
|
loading: false,
|
// 显示搜索条件
|
showSearch: true,
|
// 总条数
|
total: 0,
|
// 发票列表
|
invoiceList: [],
|
// 弹窗
|
open: false,
|
// upload:上传发票;edit:识别结果确认
|
mode: "upload",
|
title: "",
|
saving: false,
|
// 当前登录用户ID,用作默认用户ID
|
currentUserId: null,
|
// 文件上传地址与请求头
|
uploadFileUrl: process.env.VUE_APP_BASE_API + "/project/invoice/uploadOCR",
|
headers: {
|
Authorization: "Bearer " + getToken()
|
},
|
// 上传阶段表单(用户ID取当前登录用户,不在此处编辑)
|
uploadForm: {
|
caseId: ""
|
},
|
uploadRules: {
|
|
},
|
// 本次上传识别出的发票(支持一次多张)
|
uploadedList: [],
|
// 正在上传的文件数,用于判断一批上传是否结束
|
uploadingCount: 0,
|
// 识别结果 / 修改表单
|
form: {},
|
rules: {
|
|
},
|
// 文件预览
|
previewOpen: false,
|
previewUrl: "",
|
// image / pdf / other
|
previewType: "",
|
// 查询参数
|
queryParams: {
|
pageNum: 1,
|
pageSize: 10,
|
caseId: "",
|
userId: this.currentUserId,
|
invoiceName: "",
|
reimburseState: ""
|
|
}
|
};
|
},
|
computed: {
|
// 上传时携带的用户ID(当前登录用户)、案例ID(后端 uploadOCR 必传)
|
uploadData() {
|
return {
|
userId: this.currentUserId,
|
caseId: this.uploadForm.caseId
|
};
|
}
|
},
|
async created() {
|
await this.getCurrentUser();
|
this.getList();
|
},
|
methods: {
|
/** 查询发票列表 */
|
getList() {
|
this.loading = true;
|
this.queryParams.userId=this.currentUserId
|
listInvoice(this.queryParams)
|
.then(response => {
|
this.invoiceList = response.rows;
|
this.total = response.total;
|
})
|
.finally(() => {
|
this.loading = false;
|
});
|
},
|
/** 当前登录用户ID,单据的用户ID直接取该值 */
|
async getCurrentUser() {
|
try {
|
const response = await getUserProfile();
|
this.currentUserId = response.data ? response.data.userId : 0;
|
} catch (e) {
|
this.currentUserId = 0;
|
}
|
return this.currentUserId;
|
},
|
/** 案例下拉显示文本 */
|
formatCase(item) {
|
if (!item) {
|
return "";
|
}
|
return [item.donorno, item.name].filter(Boolean).join(" ");
|
},
|
|
/** 日期统一成 yyyy-MM-dd,兼容 OCR 的“2024年01月02日”等格式 */
|
normalizeDate(value) {
|
if (!value) {
|
return null;
|
}
|
const text = String(value)
|
.replace(/[年月]/g, "-")
|
.replace(/日/g, "")
|
.replace(/\//g, "-")
|
.trim();
|
const match = text.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
|
if (!match) {
|
return null;
|
}
|
return (
|
match[1] +
|
"-" +
|
String(match[2]).padStart(2, "0") +
|
"-" +
|
String(match[3]).padStart(2, "0")
|
);
|
},
|
/** 搜索按钮操作 */
|
handleQuery() {
|
this.queryParams.pageNum = 1;
|
this.getList();
|
},
|
/** 重置按钮操作 */
|
resetQuery() {
|
this.queryParams.caseId = "";
|
this.queryParams.invoiceName = "";
|
this.queryParams.reimburseState = "";
|
this.queryParams.pageNum = 1;
|
this.getList();
|
},
|
/** 上传发票按钮操作 */
|
async handleUpload() {
|
if (!this.currentUserId) {
|
await this.getCurrentUser();
|
}
|
this.uploadForm = {
|
caseId: ""
|
};
|
this.uploadedList = [];
|
this.uploadingCount = 0;
|
this.form = {};
|
this.mode = "upload";
|
this.title = "上传发票";
|
this.open = true;
|
},
|
/** 上传前校验 */
|
beforeUpload(file) {
|
if (!this.currentUserId) {
|
this.$modal.msgWarning("未获取到当前用户ID,请重新登录后再试");
|
return false;
|
}
|
const isAllowed = /\.(jpg|jpeg|png|gif|bmp|pdf)$/i.test(file.name);
|
const isLt10M = file.size / 1024 / 1024 < 10;
|
if (!isAllowed) {
|
this.$modal.msgError("仅支持图片、PDF格式的发票文件");
|
return false;
|
}
|
if (!isLt10M) {
|
this.$modal.msgError("发票文件大小不能超过10MB");
|
return false;
|
}
|
this.uploadingCount++;
|
return true;
|
},
|
/** 上传成功:后端已 OCR 并保存,结果汇总到本次上传列表 */
|
handleUploadSuccess(response) {
|
this.uploadingCount = Math.max(0, this.uploadingCount - 1);
|
if (!response || response.code !== 200) {
|
this.$modal.msgError((response && response.msg) || "发票上传失败");
|
this.afterUploadBatch();
|
return;
|
}
|
const invoiceList = response.invoiceList || [];
|
this.uploadedList = this.uploadedList.concat(invoiceList);
|
this.getList();
|
if (!invoiceList.length) {
|
this.$modal.msgWarning("该文件未识别到发票信息,请确认文件清晰");
|
} else {
|
this.$modal.msgSuccess(
|
"发票上传成功,识别到 " + invoiceList.length + " 张发票"
|
);
|
}
|
this.afterUploadBatch();
|
},
|
/** 一批上传结束后:只识别到一张发票时直接进入信息确认 */
|
afterUploadBatch() {
|
if (this.uploadingCount > 0) {
|
return;
|
}
|
if (this.uploadedList.length === 1) {
|
this.openEditForm(this.uploadedList[0]);
|
}
|
},
|
/** 上传失败 */
|
handleUploadError() {
|
this.uploadingCount = Math.max(0, this.uploadingCount - 1);
|
this.$modal.msgError("发票上传失败,请稍后重试");
|
this.afterUploadBatch();
|
},
|
/** 打开发票信息确认弹窗 */
|
openEditForm(record) {
|
this.form = {
|
id: record.id,
|
caseId: record.caseId,
|
userId: record.userId,
|
invoiceType: record.invoiceType,
|
invoiceName: record.invoiceName,
|
invoiceNo: record.invoiceNo,
|
invoiceUrl: record.invoiceUrl,
|
invoiceAmount: record.invoiceAmount,
|
occurDate: this.normalizeDate(record.occurDate),
|
invoiceStartDate: this.normalizeDate(record.invoiceStartDate),
|
invoiceEndDate: this.normalizeDate(record.invoiceEndDate),
|
sellerName: record.sellerName,
|
reimburseState: record.reimburseState || "0",
|
remark: record.remark
|
};
|
this.mode = "edit";
|
this.title = "发票信息确认";
|
this.open = true;
|
},
|
/** 修改按钮操作 */
|
handleUpdate(row) {
|
getInvoice(row.id).then(response => {
|
this.openEditForm(response.data);
|
});
|
},
|
/** 文件预览:图片、PDF 弹窗内预览,其它类型提示新窗口打开 */
|
openPreview(url) {
|
if (!url) {
|
this.$modal.msgWarning("该记录暂无发票文件");
|
return;
|
}
|
this.previewUrl = url;
|
this.previewType = this.getFileType(url);
|
this.previewOpen = true;
|
},
|
/** 根据文件地址判断预览类型 */
|
getFileType(url) {
|
const path = String(url || "")
|
.split("?")[0]
|
.toLowerCase();
|
if (/\.(jpg|jpeg|png|gif|bmp|webp)$/.test(path)) {
|
return "image";
|
}
|
if (/\.pdf$/.test(path)) {
|
return "pdf";
|
}
|
return "other";
|
},
|
/** 保存修改 */
|
submitForm() {
|
this.$refs["form"].validate(valid => {
|
if (!valid) {
|
return;
|
}
|
this.saving = true;
|
updateInvoice(this.form)
|
.then(() => {
|
this.$modal.msgSuccess("保存成功");
|
this.open = false;
|
this.getList();
|
})
|
.finally(() => {
|
this.saving = false;
|
});
|
});
|
},
|
/** 操作列“已报销”:把报销状态置为 1 */
|
handleReimburse(row) {
|
this.$modal
|
.confirm('确认发票"' + (row.invoiceName || row.invoiceNo) + '"已报销?')
|
.then(() => updateInvoice({ id: row.id, reimburseState: "1" }))
|
.then(() => {
|
this.$modal.msgSuccess("操作成功");
|
this.getList();
|
})
|
.catch(() => {});
|
},
|
/** 删除按钮操作 */
|
handleDelete(row) {
|
this.$modal
|
.confirm('是否确认删除发票"' + (row.invoiceNo || row.id) + '"?')
|
.then(() => delInvoice(row.id))
|
.then(() => {
|
this.getList();
|
this.$modal.msgSuccess("删除成功");
|
})
|
.catch(() => {});
|
}
|
}
|
};
|
</script>
|
|
<style scoped lang="scss">
|
.invoice-upload {
|
width: 100%;
|
|
::v-deep .el-upload {
|
width: 100%;
|
}
|
|
::v-deep .el-upload-dragger {
|
width: 100%;
|
}
|
}
|
|
.invoice-thumb {
|
width: 100px;
|
height: 70px;
|
margin-left: 12px;
|
border: 1px solid #ebeef5;
|
vertical-align: middle;
|
}
|
|
.invoice-preview {
|
min-height: 120px;
|
|
.preview-image {
|
display: block;
|
max-width: 100%;
|
max-height: 70vh;
|
margin: 0 auto;
|
}
|
|
.preview-frame {
|
width: 100%;
|
height: 70vh;
|
border: none;
|
}
|
|
.preview-tip {
|
padding: 40px 0;
|
text-align: center;
|
color: #909399;
|
}
|
}
|
</style>
|