feat(finance): 优化回执单上传与展示功能
- 根据支付类型动态显示"回执单"或"退款图"标题 - 重构上传流程,新增独立的上传对话框 - 支持PDF文件的预览功能,可点击查看大图 - 新增上传表单验证,包括文件类型、大小及价格确认 - 改进UI布局,提升用户体验和视觉效果 - 添加PDF预览弹窗,支持更好的文档查看体验 - 完善文件上传逻辑,确保数据完整性和一致性dev_1.0.0
parent
bd830115d4
commit
ed9fda7fe3
|
|
@ -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;">支持上传PNG、JPG、PDF文件格式</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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue