feat(finance): 优化回执单上传与展示功能

- 根据支付类型动态显示"回执单"或"退款图"标题
- 重构上传流程,新增独立的上传对话框
- 支持PDF文件的预览功能,可点击查看大图
- 新增上传表单验证,包括文件类型、大小及价格确认
- 改进UI布局,提升用户体验和视觉效果
- 添加PDF预览弹窗,支持更好的文档查看体验
- 完善文件上传逻辑,确保数据完整性和一致性
dev_1.0.0
chenhao 2025-12-11 14:45:46 +08:00
parent bd830115d4
commit ed9fda7fe3
2 changed files with 282 additions and 76 deletions

View File

@ -1,35 +1,13 @@
<template>
<el-dialog title="回执单" :visible.sync="dialogVisible" width="900px" @close="handleClose">
<el-dialog :title="titleText" :visible.sync="dialogVisible" width="900px" @close="handleClose">
<div v-if="loading" class="loading-spinner">
<i class="el-icon-loading"></i>
</div>
<div v-else class="receipt-dialog-body">
<div v-if="canUpload" class="upload-section">
<h4>上传新的回执单</h4>
<el-upload
ref="upload"
:http-request="handleUpload"
:before-upload="beforeUpload"
:file-list="fileList"
:auto-upload="false"
:limit="1"
:on-change="handleChange"
:on-remove="handleRemove"
list-type="picture-card"
accept=".pdf,.jpg,.jpeg,.png"
>
<i class="el-icon-plus"></i>
<div slot="tip" class="el-upload__tip">只能上传jpg/png/pdf文件, 且不超过2MB</div>
</el-upload>
<el-input
type="textarea"
:rows="2"
placeholder="请输入备注"
v-model="uploadRemark"
style="margin-bottom: 15px;"
></el-input>
<div v-if="canUpload" class="upload-btn-container">
<el-button type="primary" icon="el-icon-upload" @click="openUploadDialog">{{ titleText }}</el-button>
</div>
<el-timeline v-if="attachments.length > 0">
<el-timeline-item
v-for="attachment in attachments"
@ -47,7 +25,7 @@
</span>
</div>
<div class="detail-item">
<span class="item-label">回执单</span>
<span class="item-label">{{ titleText }}</span>
<div class="item-value">
<div class="image-wrapper">
<el-image
@ -57,10 +35,12 @@
style="width: 200px; height: 150px;"
fit="contain"
></el-image>
<a v-else :href="getImageUrl(attachment.filePath)" target="_blank" class="pdf-placeholder">
<i class="el-icon-document"></i>
<span>PDF - 点击预览</span>
</a>
<div v-else-if="pdfUrls[attachment.filePath]" class="pdf-thumbnail-container" @click="openPdfPreview(pdfUrls[attachment.filePath])">
<iframe :src="pdfUrls[attachment.filePath]" width="100%" height="150px" frameborder="0"></iframe>
<div class="pdf-hover-overlay">
<i class="el-icon-zoom-in"></i>
</div>
</div>
<div v-if="attachment.delFlag === '2'" class="void-overlay"></div>
</div>
<el-button
@ -69,7 +49,7 @@
class="download-btn"
icon="el-icon-download"
@click="downloadFile(attachment)"
>下载回执单</el-button>
>下载{{ titleText }}</el-button>
</div>
</div>
<div class="detail-item">
@ -85,19 +65,105 @@
</el-card>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无回执单"></el-empty>
<el-empty v-else :description="'暂无' + titleText"></el-empty>
</div>
<span slot="footer" class="dialog-footer">
<el-button v-if="canUpload" type="primary" @click="submitUpload"></el-button>
<el-button @click="dialogVisible = false">关闭</el-button>
</span>
<!-- PDF Preview Dialog -->
<el-dialog
:visible.sync="pdfPreviewVisible"
width="80%"
top="5vh"
append-to-body
custom-class="pdf-preview-dialog"
>
<iframe :src="currentPdfUrl" width="100%" height="600px" frameborder="0"></iframe>
</el-dialog>
<!-- Upload Dialog -->
<el-dialog
:title="'上传' + titleText"
:visible.sync="uploadDialogVisible"
width="70vw"
append-to-body
@close="closeUploadDialog"
custom-class="upload-receipt-dialog"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form :model="uploadForm" ref="uploadForm" label-width="120px" size="medium" >
<el-form-item label="付款方式">
<el-select v-model="uploadForm.paymentMethod" disabled style="width: 100%;">
<el-option
v-for="dict in dicts.payment_method"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="退款图" required>
<div style="display: flex; flex-direction: column; align-items: flex-start;">
<el-upload
ref="upload"
action="#"
:auto-upload="false"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
:show-file-list="false"
accept=".jpg,.jpeg,.png,.pdf"
>
<el-button size="small" type="primary" icon="el-icon-upload2">{{ uploadForm.file ? '重新上传' : '点击上传' }}</el-button>
</el-upload>
<div class="el-upload__tip" style="line-height: 1.5; margin-top: 5px;">支持上传PNGJPGPDF文件格式</div>
</div>
</el-form-item>
<el-form-item label="含税总价">
<span>{{ paymentData.totalPriceWithTax }}</span>
</el-form-item>
<el-form-item label="确认含税总价" required>
<el-input v-model="uploadForm.confirmPrice" placeholder="请输入确认含税总价"></el-input>
</el-form-item>
<el-form-item label="备注">
<el-input
type="textarea"
v-model="uploadForm.remark"
:rows="4"
placeholder="此处备注描述..."
></el-input>
</el-form-item>
</el-form>
</el-col>
<el-col :span="12">
<div class="upload-preview-container" style="height: 70vh;">
<div v-if="previewUrl" class="preview-content">
<img v-if="!isPreviewPdf" :src="previewUrl" class="preview-image" />
<iframe v-else :src="previewUrl" width="100%" height="100%" frameborder="0"></iframe>
</div>
<div v-else class="preview-placeholder">
<div class="placeholder-icon">
<i class="el-icon-picture"></i>
</div>
<div class="placeholder-text">点击图片进入预览</div>
</div>
</div>
</el-col>
</el-row>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitNewUpload"></el-button>
<el-button @click="closeUploadDialog"></el-button>
</span>
</el-dialog>
</el-dialog>
</template>
<script>
import { getPaymentAttachments, uploadPaymentAttachment } from "@/api/finance/payment";
import request from '@/utils/request';
export default {
name: "ReceiptDialog",
@ -119,8 +185,20 @@ export default {
return {
loading: false,
attachments: [],
fileList: [],
uploadRemark: "",
// Upload Dialog Data
uploadDialogVisible: false,
uploadForm: {
paymentMethod: '',
confirmPrice: '',
remark: '',
file: null
},
previewUrl: '',
isPreviewPdf: false,
// PDF Preview Data
pdfUrls: {},
pdfPreviewVisible: false,
currentPdfUrl: '',
};
},
computed: {
@ -142,6 +220,9 @@ export default {
return true;
}
return this.attachments.every(att => att.delFlag === '2');
},
titleText() {
return this.paymentData && this.paymentData.paymentBillType === 'REFUND' ? '退款图' : '回执单';
}
},
watch: {
@ -160,6 +241,7 @@ export default {
const data = response.data || [];
data.sort((a, b) => new Date(b.createTime) - new Date(a.createTime));
this.attachments = data;
this.loadPdfPreviews();
this.loading = false;
})
.catch(() => {
@ -167,6 +249,27 @@ export default {
this.loading = false;
});
},
loadPdfPreviews() {
this.attachments.forEach(att => {
if (this.isPdf(att.filePath) && !this.pdfUrls[att.filePath]) {
request({
url: '/common/download/resource',
method: 'get',
params: { resource: att.filePath },
responseType: 'blob'
}).then(res => {
const blob = new Blob([res.data], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
this.$set(this.pdfUrls, att.filePath, url);
}).catch(console.error);
}
});
},
openPdfPreview(url) {
if (!url) return;
this.currentPdfUrl = url;
this.pdfPreviewVisible = true;
},
getImageUrl(resource) {
return process.env.VUE_APP_BASE_API + "/common/download/resource?resource=" + resource;
},
@ -184,50 +287,77 @@ export default {
},
handleClose() {
this.attachments = [];
this.fileList = [];
this.uploadRemark = "";
// Clean up object URLs
Object.values(this.pdfUrls).forEach(url => URL.revokeObjectURL(url));
this.pdfUrls = {};
},
submitUpload() {
if (this.$refs.upload.uploadFiles.length === 0) {
this.$message.warning("请选择要上传的文件");
return;
}
this.$refs.upload.submit();
// New Upload Dialog Methods
openUploadDialog() {
this.uploadForm = {
paymentMethod: this.paymentData.paymentMethod,
confirmPrice: '',
remark: '',
file: null
};
this.previewUrl = '';
this.isPreviewPdf = false;
this.uploadDialogVisible = true;
},
handleRemove(file, fileList) {
this.fileList = fileList;
closeUploadDialog() {
this.uploadDialogVisible = false;
this.uploadForm.file = null;
this.previewUrl = '';
},
handleChange(file, fileList) {
this.fileList = fileList.slice(-1);
},
handleUpload(options) {
const formData = new FormData();
formData.append("file", options.file);
formData.append("relatedBillId", this.paymentData.id);
formData.append("remark", this.uploadRemark);
uploadPaymentAttachment(formData)
.then(response => {
this.$message.success("上传成功");
this.fileList = [];
this.uploadRemark = "";
this.fetchAttachments();
})
.catch(error => {
this.$message.error("上传失败");
});
},
beforeUpload(file) {
handleFileChange(file) {
const isLt2M = file.size / 1024 / 1024 < 2;
const isAcceptedType = ['image/jpeg', 'image/png', 'application/pdf'].includes(file.type);
const isAcceptedType = ['image/jpeg', 'image/png', 'application/pdf'].includes(file.raw.type);
if (!isAcceptedType) {
this.$message.error('上传文件只能是 JPG/PNG/PDF 格式!');
return;
}
if (!isLt2M) {
this.$message.error('上传文件大小不能超过 2MB!');
return;
}
return isAcceptedType && isLt2M;
this.uploadForm.file = file.raw;
this.isPreviewPdf = file.raw.type === 'application/pdf';
this.previewUrl = URL.createObjectURL(file.raw);
},
handleFileRemove() {
this.uploadForm.file = null;
this.previewUrl = '';
},
submitNewUpload() {
if (!this.uploadForm.file) {
this.$message.warning("请选择要上传的文件");
return;
}
if (!this.uploadForm.confirmPrice) {
this.$message.warning("请输入确认含税总价");
return;
}
if (parseFloat(this.uploadForm.confirmPrice) !== parseFloat(this.paymentData.totalPriceWithTax)) {
this.$message.error("确认含税总价与原含税总价不一致");
return;
}
const formData = new FormData();
formData.append("file", this.uploadForm.file);
formData.append("relatedBillId", this.paymentData.id);
formData.append("remark", this.uploadForm.remark);
uploadPaymentAttachment(formData)
.then(response => {
this.$message.success("上传成功");
this.closeUploadDialog();
this.fetchAttachments();
})
.catch(error => {
this.$message.error("上传失败");
});
},
},
};
@ -289,10 +419,8 @@ export default {
.download-btn {
display: block;
}
.upload-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
.upload-btn-container {
margin-bottom: 20px;
}
.pdf-placeholder {
display: flex;
@ -314,4 +442,82 @@ export default {
font-size: 48px;
margin-bottom: 5px;
}
/* New Dialog Styles */
.upload-preview-container {
width: 100%;
height: 300px;
background-color: #f5f7fa;
border: 1px solid #e4e7ed;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.preview-content {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.preview-pdf {
display: flex;
flex-direction: column;
align-items: center;
font-size: 16px;
color: #606266;
}
.preview-pdf .el-icon-document {
font-size: 64px;
margin-bottom: 10px;
}
.preview-placeholder {
text-align: center;
color: #909399;
}
.placeholder-icon {
font-size: 64px;
margin-bottom: 10px;
color: #c0c4cc;
}
.placeholder-text {
font-size: 14px;
}
.pdf-thumbnail-container {
position: relative;
width: 100%;
height: 150px;
cursor: pointer;
}
.pdf-hover-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s;
color: #fff;
font-size: 24px;
pointer-events: none; /* Let clicks pass through to container, but this is overlay on top of iframe so actually we want it to capture clicks if iframe swallows them? */
/* Actually, if pointer-events is none, the click goes to the iframe and iframe swallows it. */
/* So we want pointer-events: auto on the overlay, or just rely on the container. */
/* If container has the click listener, and overlay covers everything, overlay needs to propagate click or handle it. */
/* A simple way: make overlay clickable. */
pointer-events: auto;
}
.pdf-thumbnail-container:hover .pdf-hover-overlay {
opacity: 1;
}
</style>

View File

@ -180,7 +180,7 @@
type="text"
icon="el-icon-money"
@click="handleReceipt(scope.row)"
>回执单</el-button>
>{{ scope.row.paymentBillType === 'REFUND' ? '退款图' : '回执单' }}</el-button>
<el-button
size="mini"
type="text"