feat(finance): 优化回执单上传与展示功能
- 根据支付类型动态显示"回执单"或"退款图"标题 - 重构上传流程,新增独立的上传对话框 - 支持PDF文件的预览功能,可点击查看大图 - 新增上传表单验证,包括文件类型、大小及价格确认 - 改进UI布局,提升用户体验和视觉效果 - 添加PDF预览弹窗,支持更好的文档查看体验 - 完善文件上传逻辑,确保数据完整性和一致性dev_1.0.0
parent
bd830115d4
commit
ed9fda7fe3
|
|
@ -1,35 +1,13 @@
|
||||||
<template>
|
<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">
|
<div v-if="loading" class="loading-spinner">
|
||||||
<i class="el-icon-loading"></i>
|
<i class="el-icon-loading"></i>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="receipt-dialog-body">
|
<div v-else class="receipt-dialog-body">
|
||||||
<div v-if="canUpload" class="upload-section">
|
<div v-if="canUpload" class="upload-btn-container">
|
||||||
<h4>上传新的回执单</h4>
|
<el-button type="primary" icon="el-icon-upload" @click="openUploadDialog">上传{{ titleText }}</el-button>
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<el-timeline v-if="attachments.length > 0">
|
<el-timeline v-if="attachments.length > 0">
|
||||||
<el-timeline-item
|
<el-timeline-item
|
||||||
v-for="attachment in attachments"
|
v-for="attachment in attachments"
|
||||||
|
|
@ -47,7 +25,7 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<span class="item-label">回执单</span>
|
<span class="item-label">{{ titleText }}</span>
|
||||||
<div class="item-value">
|
<div class="item-value">
|
||||||
<div class="image-wrapper">
|
<div class="image-wrapper">
|
||||||
<el-image
|
<el-image
|
||||||
|
|
@ -57,10 +35,12 @@
|
||||||
style="width: 200px; height: 150px;"
|
style="width: 200px; height: 150px;"
|
||||||
fit="contain"
|
fit="contain"
|
||||||
></el-image>
|
></el-image>
|
||||||
<a v-else :href="getImageUrl(attachment.filePath)" target="_blank" class="pdf-placeholder">
|
<div v-else-if="pdfUrls[attachment.filePath]" class="pdf-thumbnail-container" @click="openPdfPreview(pdfUrls[attachment.filePath])">
|
||||||
<i class="el-icon-document"></i>
|
<iframe :src="pdfUrls[attachment.filePath]" width="100%" height="150px" frameborder="0"></iframe>
|
||||||
<span>PDF - 点击预览</span>
|
<div class="pdf-hover-overlay">
|
||||||
</a>
|
<i class="el-icon-zoom-in"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="attachment.delFlag === '2'" class="void-overlay">作废</div>
|
<div v-if="attachment.delFlag === '2'" class="void-overlay">作废</div>
|
||||||
</div>
|
</div>
|
||||||
<el-button
|
<el-button
|
||||||
|
|
@ -69,7 +49,7 @@
|
||||||
class="download-btn"
|
class="download-btn"
|
||||||
icon="el-icon-download"
|
icon="el-icon-download"
|
||||||
@click="downloadFile(attachment)"
|
@click="downloadFile(attachment)"
|
||||||
>下载回执单</el-button>
|
>下载{{ titleText }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
|
|
@ -85,19 +65,105 @@
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-timeline-item>
|
</el-timeline-item>
|
||||||
</el-timeline>
|
</el-timeline>
|
||||||
<el-empty v-else description="暂无回执单"></el-empty>
|
<el-empty v-else :description="'暂无' + titleText"></el-empty>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<span slot="footer" class="dialog-footer">
|
<span slot="footer" class="dialog-footer">
|
||||||
<el-button v-if="canUpload" type="primary" @click="submitUpload">提交上传</el-button>
|
|
||||||
<el-button @click="dialogVisible = false">关闭</el-button>
|
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||||
</span>
|
</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>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getPaymentAttachments, uploadPaymentAttachment } from "@/api/finance/payment";
|
import { getPaymentAttachments, uploadPaymentAttachment } from "@/api/finance/payment";
|
||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ReceiptDialog",
|
name: "ReceiptDialog",
|
||||||
|
|
@ -119,8 +185,20 @@ export default {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
fileList: [],
|
// Upload Dialog Data
|
||||||
uploadRemark: "",
|
uploadDialogVisible: false,
|
||||||
|
uploadForm: {
|
||||||
|
paymentMethod: '',
|
||||||
|
confirmPrice: '',
|
||||||
|
remark: '',
|
||||||
|
file: null
|
||||||
|
},
|
||||||
|
previewUrl: '',
|
||||||
|
isPreviewPdf: false,
|
||||||
|
// PDF Preview Data
|
||||||
|
pdfUrls: {},
|
||||||
|
pdfPreviewVisible: false,
|
||||||
|
currentPdfUrl: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -142,6 +220,9 @@ export default {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return this.attachments.every(att => att.delFlag === '2');
|
return this.attachments.every(att => att.delFlag === '2');
|
||||||
|
},
|
||||||
|
titleText() {
|
||||||
|
return this.paymentData && this.paymentData.paymentBillType === 'REFUND' ? '退款图' : '回执单';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
@ -160,6 +241,7 @@ export default {
|
||||||
const data = response.data || [];
|
const data = response.data || [];
|
||||||
data.sort((a, b) => new Date(b.createTime) - new Date(a.createTime));
|
data.sort((a, b) => new Date(b.createTime) - new Date(a.createTime));
|
||||||
this.attachments = data;
|
this.attachments = data;
|
||||||
|
this.loadPdfPreviews();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|
@ -167,6 +249,27 @@ export default {
|
||||||
this.loading = false;
|
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) {
|
getImageUrl(resource) {
|
||||||
return process.env.VUE_APP_BASE_API + "/common/download/resource?resource=" + resource;
|
return process.env.VUE_APP_BASE_API + "/common/download/resource?resource=" + resource;
|
||||||
},
|
},
|
||||||
|
|
@ -184,51 +287,78 @@ export default {
|
||||||
},
|
},
|
||||||
handleClose() {
|
handleClose() {
|
||||||
this.attachments = [];
|
this.attachments = [];
|
||||||
this.fileList = [];
|
// Clean up object URLs
|
||||||
this.uploadRemark = "";
|
Object.values(this.pdfUrls).forEach(url => URL.revokeObjectURL(url));
|
||||||
|
this.pdfUrls = {};
|
||||||
},
|
},
|
||||||
submitUpload() {
|
// New Upload Dialog Methods
|
||||||
if (this.$refs.upload.uploadFiles.length === 0) {
|
openUploadDialog() {
|
||||||
|
this.uploadForm = {
|
||||||
|
paymentMethod: this.paymentData.paymentMethod,
|
||||||
|
confirmPrice: '',
|
||||||
|
remark: '',
|
||||||
|
file: null
|
||||||
|
};
|
||||||
|
this.previewUrl = '';
|
||||||
|
this.isPreviewPdf = false;
|
||||||
|
this.uploadDialogVisible = true;
|
||||||
|
},
|
||||||
|
closeUploadDialog() {
|
||||||
|
this.uploadDialogVisible = false;
|
||||||
|
this.uploadForm.file = null;
|
||||||
|
this.previewUrl = '';
|
||||||
|
},
|
||||||
|
handleFileChange(file) {
|
||||||
|
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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("请选择要上传的文件");
|
this.$message.warning("请选择要上传的文件");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$refs.upload.submit();
|
if (!this.uploadForm.confirmPrice) {
|
||||||
},
|
this.$message.warning("请输入确认含税总价");
|
||||||
handleRemove(file, fileList) {
|
return;
|
||||||
this.fileList = fileList;
|
}
|
||||||
},
|
if (parseFloat(this.uploadForm.confirmPrice) !== parseFloat(this.paymentData.totalPriceWithTax)) {
|
||||||
handleChange(file, fileList) {
|
this.$message.error("确认含税总价与原含税总价不一致");
|
||||||
this.fileList = fileList.slice(-1);
|
return;
|
||||||
},
|
}
|
||||||
handleUpload(options) {
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", options.file);
|
formData.append("file", this.uploadForm.file);
|
||||||
formData.append("relatedBillId", this.paymentData.id);
|
formData.append("relatedBillId", this.paymentData.id);
|
||||||
formData.append("remark", this.uploadRemark);
|
formData.append("remark", this.uploadForm.remark);
|
||||||
|
|
||||||
uploadPaymentAttachment(formData)
|
uploadPaymentAttachment(formData)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.$message.success("上传成功");
|
this.$message.success("上传成功");
|
||||||
this.fileList = [];
|
this.closeUploadDialog();
|
||||||
this.uploadRemark = "";
|
|
||||||
this.fetchAttachments();
|
this.fetchAttachments();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.$message.error("上传失败");
|
this.$message.error("上传失败");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
beforeUpload(file) {
|
|
||||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
|
||||||
const isAcceptedType = ['image/jpeg', 'image/png', 'application/pdf'].includes(file.type);
|
|
||||||
|
|
||||||
if (!isAcceptedType) {
|
|
||||||
this.$message.error('上传文件只能是 JPG/PNG/PDF 格式!');
|
|
||||||
}
|
|
||||||
if (!isLt2M) {
|
|
||||||
this.$message.error('上传文件大小不能超过 2MB!');
|
|
||||||
}
|
|
||||||
return isAcceptedType && isLt2M;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -289,10 +419,8 @@ export default {
|
||||||
.download-btn {
|
.download-btn {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.upload-section {
|
.upload-btn-container {
|
||||||
margin-top: 20px;
|
margin-bottom: 20px;
|
||||||
padding-top: 20px;
|
|
||||||
border-top: 1px solid #eee;
|
|
||||||
}
|
}
|
||||||
.pdf-placeholder {
|
.pdf-placeholder {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -314,4 +442,82 @@ export default {
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
margin-bottom: 5px;
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,7 @@
|
||||||
type="text"
|
type="text"
|
||||||
icon="el-icon-money"
|
icon="el-icon-money"
|
||||||
@click="handleReceipt(scope.row)"
|
@click="handleReceipt(scope.row)"
|
||||||
>回执单</el-button>
|
>{{ scope.row.paymentBillType === 'REFUND' ? '退款图' : '回执单' }}</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
size="mini"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue