feat: 多个页面功能优化与新增

1.  异议页面:新增状态为4时的导出PDF按钮,替换原有注释的打印按钮
2.  流程图页面:重构流程图组件,支持多流程切换、节点点击事件,优化主题配置和渲染逻辑
3.  钢卷待领页面:新增钢卷数据修正功能,新增操作列按钮和对应弹窗表单,注释绑定组件
4.  导出PDF弹窗:优化多钢卷数据展示,拆分合并附件排版,优化导出样式和分页逻辑
This commit is contained in:
2026-06-30 11:42:03 +08:00
parent 89773b273b
commit 81dec034b3
4 changed files with 873 additions and 330 deletions

View File

@@ -4,7 +4,7 @@
<div v-loading="loading" class="export-preview">
<div ref="pdfContent" class="pdf-content">
<!-- ========== 附件一质量异议反馈单 ========== -->
<div class="attachment-section">
<div ref="feedbackFormSection" class="attachment-section">
<h2 class="doc-title">质量异议反馈单</h2>
<table class="form-table">
@@ -24,19 +24,19 @@
<tr>
<td class="label" style="width:60px;vertical-align:middle" :rowspan="8">情况概述<br>用户提供</td>
<td class="label" style="width:120px">产品名称</td>
<td style="width:180px">{{ firstCoil.itemName || '' }}</td>
<td style="width:180px">{{ allItemNames }}</td>
<td class="label" style="width:110px">异议合同号</td>
<td>{{ firstContract.contractCode || '' }}</td>
<td>{{ allContractCodes }}</td>
</tr>
<tr>
<td class="label">合同交货量</td>
<td>{{ firstContract.orderAmount || '' }}</td>
<td>{{ allOrderAmounts }}</td>
<td class="label">牌号/钢种</td>
<td>{{ firstCoil.material || '' }}</td>
<td>{{ allMaterials }}</td>
</tr>
<tr>
<td class="label">规格</td>
<td>{{ firstCoil.specification || '' }}</td>
<td>{{ allSpecifications }}</td>
<td class="label">异议量</td>
<td>{{ coilWeightSummary }}</td>
</tr>
@@ -44,7 +44,7 @@
<td class="label">采购日期</td>
<td>{{ detail.complaintDate | formatDate }}</td>
<td class="label">使用日期</td>
<td>{{ firstCoil.exportTime | formatDate }}</td>
<td>{{ allExportTimes }}</td>
</tr>
<tr>
<td class="label">产品卷号</td>
@@ -52,7 +52,7 @@
</tr>
<tr>
<td class="label">产品生产日期</td>
<td colspan="3">{{ firstCoil.createTime | formatDate }}</td>
<td colspan="3">{{ allCreateTimes }}</td>
</tr>
<tr>
<td class="label">下游使用用户名称</td>
@@ -95,174 +95,176 @@
</div>
</div>
<!-- ========== 附件二质量投诉立案确认及处置单 ========== -->
<div class="page-break"></div>
<div class="attachment-section">
<h2 class="doc-title">附件 2</h2>
<h3 class="sub-title">质量投诉立案确认及处置单</h3>
<!-- ========== 附件二 + 附件三 ========== -->
<div ref="restContentSection">
<div class="page-break"></div>
<div class="attachment-section">
<h2 class="doc-title">附件 2</h2>
<h3 class="sub-title">质量投诉立案确认及处置单</h3>
<table class="form-table">
<tr>
<td class="label" style="width:120px">收到日期</td>
<td style="width:180px">{{ detail.complaintDate | formatDate }}</td>
<td class="label" style="width:100px">投诉单编号</td>
<td>{{ detail.complaintNo || '' }}</td>
</tr>
<tr>
<td class="label">异议反馈单位</td>
<td>{{ customer.companyName || '' }}</td>
<td class="label">反馈人姓名</td>
<td>{{ customer.contactPerson || '' }}</td>
</tr>
<tr>
<td class="label">信息接收人</td>
<td>{{ detail.auditOpinion || '' }}</td>
<td class="label">联系方式</td>
<td>{{ customer.contactWay || '' }}</td>
</tr>
<tr>
<td class="label">订货用户及使用<br>用户名称及地址</td>
<td colspan="3">{{ customer.companyName || '' }} {{ customer.address || '' }}</td>
</tr>
<tr>
<td class="label">产品名称</td>
<td>{{ firstCoil.itemName || '' }}</td>
<td class="label">合同号</td>
<td>{{ firstContract.contractCode || '' }}</td>
</tr>
<tr>
<td class="label">合同交货量</td>
<td>{{ firstContract.orderAmount || '' }}</td>
<td class="label">未出库量</td>
<td></td>
</tr>
<tr>
<td class="label">规格mm</td>
<td>{{ firstCoil.specification || '' }}</td>
<td class="label">产品钢种/牌号</td>
<td>{{ firstCoil.material || '' }}</td>
</tr>
<tr>
<td class="label">异议量</td>
<td>{{ coilWeightSummary }}</td>
<td class="label">产品用途</td>
<td>{{ detail.productUsage || '' }}</td>
</tr>
</table>
<table class="form-table">
<tr>
<td class="label" style="width:120px">收到日期</td>
<td style="width:180px">{{ detail.complaintDate | formatDate }}</td>
<td class="label" style="width:100px">投诉单编号</td>
<td>{{ detail.complaintNo || '' }}</td>
</tr>
<tr>
<td class="label">异议反馈单位</td>
<td>{{ customer.companyName || '' }}</td>
<td class="label">反馈人姓名</td>
<td>{{ customer.contactPerson || '' }}</td>
</tr>
<tr>
<td class="label">信息接收人</td>
<td>{{ detail.auditOpinion || '' }}</td>
<td class="label">联系方式</td>
<td>{{ customer.contactWay || '' }}</td>
</tr>
<tr>
<td class="label">订货用户及使用<br>用户名称及地址</td>
<td colspan="3">{{ customer.companyName || '' }} {{ customer.address || '' }}</td>
</tr>
<tr>
<td class="label">产品名称</td>
<td>{{ allItemNames }}</td>
<td class="label">合同号</td>
<td>{{ allContractCodes }}</td>
</tr>
<tr>
<td class="label">合同交货量</td>
<td>{{ allOrderAmounts }}</td>
<td class="label">未出库量</td>
<td></td>
</tr>
<tr>
<td class="label">规格mm</td>
<td>{{ allSpecifications }}</td>
<td class="label">产品钢种/牌号</td>
<td>{{ allMaterials }}</td>
</tr>
<tr>
<td class="label">异议量</td>
<td>{{ coilWeightSummary }}</td>
<td class="label">产品用途</td>
<td>{{ detail.productUsage || '' }}</td>
</tr>
</table>
<div class="complaint-desc">
<div class="desc-label">问题描述产品卷号产品生产日期缺陷名称缺陷描述缺陷程度缺陷数量发生问题的生产工序等以及附加缺陷照片</div>
<div class="desc-content">客户反馈{{ detail.complaintContent || '' }}</div>
</div>
<table class="form-table">
<tr>
<td class="label" style="width:100px;vertical-align:middle" :rowspan="2">问题内/<br>外部调<br><br>确认</td>
<td class="label" style="width:60px">内部</td>
<td>
<div>{{ prodOpinion.internalInvestigation || '' }}</div>
<div style="text-align:right;margin-top:8px">
确认人{{ prodOpinion.internalConfirmer || '' }} 日期{{ prodOpinion.internalConfirmDate || '' }}
</div>
</td>
</tr>
<tr>
<td class="label">外部</td>
<td>
<div>{{ prodOpinion.externalInvestigation || '' }}</div>
<div style="text-align:right;margin-top:8px">
确认人{{ prodOpinion.externalConfirmer || '' }} 日期{{ prodOpinion.externalConfirmDate || '' }}
</div>
</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:100px">责任单位</td>
<td style="width:200px">{{ prodOpinion.responsibleUnit || '' }}</td>
<td class="label" style="width:100px">接收人/日期</td>
<td>{{ prodOpinion.receiver || '' }} {{ prodOpinion.acceptDate || '' }}</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:100px;vertical-align:middle" :rowspan="2">原因分析</td>
<td class="label" style="width:70px">产生原因</td>
<td>{{ qualityOpinion.cause || '' }}</td>
</tr>
<tr>
<td class="label">流出原因</td>
<td>{{ qualityOpinion.escapeReason || '' }}</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:100px">纠正/预防措施</td>
<td>{{ qualityOpinion.correctiveAction || '' }}</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:100px">整改计划完成时间</td>
<td style="width:200px">{{ qualityOpinion.rectifyDate || '' }}</td>
<td class="label" style="width:100px">责任人签字/日期</td>
<td></td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:120px">品质部主管审核</td>
<td>{{ qualityOpinion.supervisorOpinion || '' }}</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:70px;vertical-align:middle">意见</td>
<td>
<div style="text-align:right">签字{{ qualityOpinion.internalConfirmer || '' }} 日期{{ qualityOpinion.internalConfirmDate || '' }}</div>
</td>
</tr>
<tr>
<td class="label" style="vertical-align:middle">商务<br>处置</td>
<td>
<table class="inner-table">
<tr>
<td class="label" style="width:100px">销售人员处<br>意见</td>
<td>{{ salesOpinion.handlingOpinion || '' }}</td>
</tr>
<tr>
<td class="label">销售部领导<br>审核意见</td>
<td>{{ salesOpinion.leaderOpinion || '' }}</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="label" style="vertical-align:middle">公司领导审批意<br></td>
<td>
<div style="min-height:40px">{{ detail.planContent || '' }}</div>
<div style="text-align:right;margin-top:8px">签字  日期</div>
</td>
</tr>
</table>
</div>
<!-- ========== 附件三缺陷图片 ========== -->
<div class="page-break"></div>
<div class="attachment-section">
<h2 class="doc-title">附缺陷照片</h2>
<div v-if="defectImages.length > 0" class="defect-grid">
<div v-for="(img, idx) in defectImages" :key="idx" class="defect-item">
<img :src="img" class="defect-img" />
<div class="complaint-desc">
<div class="desc-label">问题描述产品卷号产品生产日期缺陷名称缺陷描述缺陷程度缺陷数量发生问题的生产工序等以及附加缺陷照片</div>
<div class="desc-content">客户反馈{{ detail.complaintContent || '' }}</div>
</div>
<table class="form-table">
<tr>
<td class="label" style="width:100px;vertical-align:middle" :rowspan="2">问题内/<br>外部调<br><br>确认</td>
<td class="label" style="width:60px">内部</td>
<td>
<div>{{ prodOpinion.internalInvestigation || '' }}</div>
<div style="text-align:right;margin-top:8px">
确认人{{ prodOpinion.internalConfirmer || '' }} 日期{{ prodOpinion.internalConfirmDate || '' }}
</div>
</td>
</tr>
<tr>
<td class="label">外部</td>
<td>
<div>{{ prodOpinion.externalInvestigation || '' }}</div>
<div style="text-align:right;margin-top:8px">
确认人{{ prodOpinion.externalConfirmer || '' }} 日期{{ prodOpinion.externalConfirmDate || '' }}
</div>
</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:100px">责任单位</td>
<td style="width:200px">{{ prodOpinion.responsibleUnit || '' }}</td>
<td class="label" style="width:100px">接收人/日期</td>
<td>{{ prodOpinion.receiver || '' }} {{ prodOpinion.acceptDate || '' }}</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:100px;vertical-align:middle" :rowspan="2">原因分析</td>
<td class="label" style="width:70px">产生原因</td>
<td>{{ qualityOpinion.cause || '' }}</td>
</tr>
<tr>
<td class="label">流出原因</td>
<td>{{ qualityOpinion.escapeReason || '' }}</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:100px">纠正/预防措施</td>
<td>{{ qualityOpinion.correctiveAction || '' }}</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:100px">整改计划完成时间</td>
<td style="width:200px">{{ qualityOpinion.rectifyDate || '' }}</td>
<td class="label" style="width:100px">责任人签字/日期</td>
<td></td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:120px">品质部主管审核</td>
<td>{{ qualityOpinion.supervisorOpinion || '' }}</td>
</tr>
</table>
<table class="form-table" style="margin-top:-1px">
<tr>
<td class="label" style="width:70px;vertical-align:middle">意见</td>
<td>
<div style="text-align:right">签字{{ qualityOpinion.internalConfirmer || '' }} 日期{{ qualityOpinion.internalConfirmDate || '' }}</div>
</td>
</tr>
<tr>
<td class="label" style="vertical-align:middle">商务<br>处置</td>
<td>
<table class="inner-table">
<tr>
<td class="label" style="width:100px">销售人员处<br>意见</td>
<td>{{ salesOpinion.handlingOpinion || '' }}</td>
</tr>
<tr>
<td class="label">销售部领导<br>审核意见</td>
<td>{{ salesOpinion.leaderOpinion || '' }}</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="label" style="vertical-align:middle">公司领导审批意<br></td>
<td>
<div style="min-height:40px">{{ detail.planContent || '' }}</div>
<div style="text-align:right;margin-top:8px">签字  日期</div>
</td>
</tr>
</table>
</div>
<!-- ========== 附件三缺陷图片 ========== -->
<div class="page-break"></div>
<div class="attachment-section">
<h2 class="doc-title">附缺陷照片</h2>
<div v-if="defectImages.length > 0" class="defect-grid">
<div v-for="(img, idx) in defectImages" :key="idx" class="defect-item">
<img :src="img" class="defect-img" />
</div>
</div>
<div v-else class="empty-tip">暂无缺陷照片</div>
</div>
<div v-else class="empty-tip">暂无缺陷照片</div>
</div>
</div>
</div>
@@ -322,16 +324,42 @@ export default {
const day = String(d.getDate()).padStart(2, '0');
return `${y}.${m}.${day}`;
},
firstCoil() {
if (this.coilList.length === 0) return {};
const rel = this.coilList[0];
return rel.coilInfo || {};
allItemNames() {
return [...new Set(this.coilList.map(rel => (rel.coilInfo && rel.coilInfo.itemName) || '').filter(Boolean))].join('、');
},
firstContract() {
if (this.coilList.length === 0) return {};
const rel = this.coilList[0];
const orders = (rel.coilInfo && rel.coilInfo.orderList) || [];
return orders[0] || {};
allMaterials() {
return [...new Set(this.coilList.map(rel => (rel.coilInfo && rel.coilInfo.material) || '').filter(Boolean))].join('、');
},
allSpecifications() {
return [...new Set(this.coilList.map(rel => (rel.coilInfo && rel.coilInfo.specification) || '').filter(Boolean))].join('、');
},
allContractCodes() {
const codes = [];
this.coilList.forEach(rel => {
const orders = (rel.coilInfo && rel.coilInfo.orderList) || [];
orders.forEach(o => { if (o.contractCode) codes.push(o.contractCode); });
});
return [...new Set(codes)].join('、');
},
allOrderAmounts() {
const amounts = [];
this.coilList.forEach(rel => {
const orders = (rel.coilInfo && rel.coilInfo.orderList) || [];
orders.forEach(o => { if (o.orderAmount != null) amounts.push(o.orderAmount); });
});
return [...new Set(amounts)].join('、');
},
allExportTimes() {
return this.coilList
.map(rel => this.fmtDate((rel.coilInfo && rel.coilInfo.exportTime) || ''))
.filter(Boolean)
.join('、');
},
allCreateTimes() {
return this.coilList
.map(rel => this.fmtDate((rel.coilInfo && rel.coilInfo.createTime) || ''))
.filter(Boolean)
.join('、');
},
coilNoSummary() {
return this.coilList
@@ -417,43 +445,35 @@ export default {
this.taskList = [];
this.defectImages = [];
},
fmtDate(val) {
if (!val) return '';
const d = new Date(val);
if (isNaN(d.getTime())) return val;
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}.${m}.${day}`;
},
async exportPdf() {
const element = this.$refs.pdfContent;
if (!element) return;
const feedbackEl = this.$refs.feedbackFormSection;
const restEl = this.$refs.restContentSection;
if (!feedbackEl || !restEl) return;
this.exporting = true;
try {
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
logging: false,
backgroundColor: '#ffffff',
windowWidth: 794
});
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF('p', 'mm', 'a4');
const pdfWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const imgWidth = pdfWidth;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
const pageWidth = pdf.internal.pageSize.getWidth(); // 210
const pageHeight = pdf.internal.pageSize.getHeight(); // 297
const margin = 10; // mm
const contentWidth = pageWidth - margin * 2; // 190
const contentHeight = pageHeight - margin * 2; // 277
// 附件一:质量异议反馈单 — 独立起始
await this._renderSection(pdf, feedbackEl, pageWidth, contentWidth, contentHeight, margin);
// 附件二 + 附件三 — 从新页开始
pdf.addPage();
await this._renderSection(pdf, restEl, pageWidth, contentWidth, contentHeight, margin);
if (imgHeight <= pageHeight) {
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
} else {
let posY = 0;
const ratio = pdfWidth / canvas.width;
const pageCanvasHeight = pageHeight / ratio;
while (posY < canvas.height) {
if (posY > 0) pdf.addPage();
const pieceCanvas = document.createElement('canvas');
pieceCanvas.width = canvas.width;
pieceCanvas.height = Math.min(pageCanvasHeight, canvas.height - posY);
const ctx = pieceCanvas.getContext('2d');
ctx.drawImage(canvas, 0, posY, canvas.width, pieceCanvas.height, 0, 0, canvas.width, pieceCanvas.height);
const pieceData = pieceCanvas.toDataURL('image/png');
pdf.addImage(pieceData, 'PNG', 0, 0, imgWidth, pieceCanvas.height * ratio);
posY += pageCanvasHeight;
}
}
pdf.save(`${this.detail.complaintNo || '售后单'}_质量异议全套资料.pdf`);
this.$modal.msgSuccess('PDF导出成功');
this.visible = false;
@@ -463,6 +483,38 @@ export default {
} finally {
this.exporting = false;
}
},
async _renderSection(pdf, el, pageWidth, contentWidth, contentHeight, margin) {
const canvas = await html2canvas(el, {
scale: 2,
useCORS: true,
logging: false,
backgroundColor: '#ffffff',
windowWidth: 794
});
const ratio = contentWidth / canvas.width;
const imgFullHeight = canvas.height * ratio;
if (imgFullHeight <= contentHeight) {
// 单页:居中放置
pdf.addImage(canvas.toDataURL('image/png'), 'PNG', margin, margin, contentWidth, imgFullHeight);
return;
}
// 多页分割
let posY = 0;
const pageCanvasHeight = contentHeight / ratio;
while (posY < canvas.height) {
if (posY > 0) pdf.addPage();
const pieceHeight = Math.min(pageCanvasHeight, canvas.height - posY);
const pieceCanvas = document.createElement('canvas');
pieceCanvas.width = canvas.width;
pieceCanvas.height = pieceHeight;
const ctx = pieceCanvas.getContext('2d');
ctx.drawImage(canvas, 0, posY, canvas.width, pieceHeight, 0, 0, canvas.width, pieceHeight);
pdf.addImage(pieceCanvas.toDataURL('image/png'), 'PNG', margin, margin, contentWidth, pieceHeight * ratio);
posY += pageCanvasHeight;
}
}
}
};
@@ -472,7 +524,7 @@ export default {
.export-preview {
max-height: 70vh;
overflow-y: auto;
background: #f5f5f5;
background: #ffffff;
padding: 16px;
}
@@ -493,7 +545,7 @@ export default {
break-after: page;
height: 1px;
margin: 20px 0;
border-top: 1px dashed #ccc;
border-top: 1px dashed #000;
}
.doc-title {
@@ -530,7 +582,6 @@ export default {
}
.form-table .label {
background-color: #f5f5f5;
font-weight: 500;
text-align: center;
vertical-align: middle;
@@ -558,7 +609,7 @@ export default {
.desc-label {
font-size: 12px;
color: #333;
color: #000;
margin-bottom: 8px;
}
@@ -581,7 +632,6 @@ export default {
}
.company-header {
background-color: #f5f5f5;
text-align: center;
font-weight: 700;
font-size: 14px;
@@ -597,7 +647,7 @@ export default {
.defect-item {
flex: 0 0 calc(50% - 6px);
border: 1px solid #ddd;
border: 1px solid #000;
padding: 4px;
}
@@ -610,7 +660,7 @@ export default {
.empty-tip {
text-align: center;
color: #999;
color: #000;
padding: 40px;
font-size: 14px;
}

View File

@@ -70,8 +70,8 @@
<HeaderControlSection :complaintNo="currentRow.complaintNo" :flowStatus="currentRow.flowStatus"
:meta="currentRow">
<template #actions>
<!-- <el-button :loading="pdfLoading" size="mini" type="text" icon="el-icon-printer" @click="handlePrint"
:disabled="pdfLoading" title="导出PDF">导出PDF</el-button> -->
<el-button v-if="currentRow.flowStatus === 4" size="mini" type="text" icon="el-icon-download"
@click="handleExportPdf(currentRow)" title="导出PDF">导出</el-button>
<el-button v-if="currentRow.flowStatus === 1" size="mini" type="primary" plain
icon="el-icon-s-promotion" @click="handleOpinionDispatch">意见下发</el-button>
<el-button v-if="false" size="mini" type="warning" plain