refactor(contract/export/preview): 优化合同打印导出功能,添加页码和修复样式
1. 移除coilTable的异常行高亮逻辑 2. 修复导航栏告警badge的显示逻辑 3. 为合同导出和预览添加页码标注,调整打印样式 4. 移除冗余的合同打印预览代码
This commit is contained in:
@@ -12,7 +12,7 @@
|
|||||||
<el-icon class="el-icon-document-checked" />
|
<el-icon class="el-icon-document-checked" />
|
||||||
</div>
|
</div>
|
||||||
<div class="right-menu-item hover-effect" @click="gotoWarning" title="告警信息">
|
<div class="right-menu-item hover-effect" @click="gotoWarning" title="告警信息">
|
||||||
<el-badge :is-dot="hasWarning" class="nav-badge">
|
<el-badge :value="hasWarning || ''" class="nav-badge">
|
||||||
<el-icon class="el-icon-bell" />
|
<el-icon class="el-icon-bell" />
|
||||||
</el-badge>
|
</el-badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,9 +116,9 @@ export default {
|
|||||||
},
|
},
|
||||||
checkWarning() {
|
checkWarning() {
|
||||||
listMaterialWarning({ pageNum: 1, pageSize: 1, warningStatus: '0' }).then(response => {
|
listMaterialWarning({ pageNum: 1, pageSize: 1, warningStatus: '0' }).then(response => {
|
||||||
this.hasWarning = response.total > 0
|
this.hasWarning = response.total
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.hasWarning = false
|
this.hasWarning = 0
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async logout() {
|
async logout() {
|
||||||
|
|||||||
@@ -404,7 +404,9 @@ export default {
|
|||||||
const printW = pW - mg * 2;
|
const printW = pW - mg * 2;
|
||||||
const printH = pH - mg * 2;
|
const printH = pH - mg * 2;
|
||||||
const ratio = canvas.width / printW;
|
const ratio = canvas.width / printW;
|
||||||
const pageCanvasHeight = printH * ratio;
|
const footerMargin = 8;
|
||||||
|
const imagePrintH = printH - footerMargin;
|
||||||
|
const pageCanvasHeight = imagePrintH * ratio;
|
||||||
|
|
||||||
let currentY = 0;
|
let currentY = 0;
|
||||||
const pages = [];
|
const pages = [];
|
||||||
@@ -435,17 +437,31 @@ export default {
|
|||||||
currentY = pageBottom;
|
currentY = pageBottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkedAttachmentCount = this.attachmentConfigs.filter(a => a.checked).length;
|
||||||
|
const totalPages = pages.length + checkedAttachmentCount;
|
||||||
|
let pageNum = 0;
|
||||||
|
|
||||||
|
const fullPageCanvasHeight = Math.round(printH * ratio);
|
||||||
|
|
||||||
for (let i = 0; i < pages.length; i++) {
|
for (let i = 0; i < pages.length; i++) {
|
||||||
|
pageNum++;
|
||||||
const { top, bottom } = pages[i];
|
const { top, bottom } = pages[i];
|
||||||
const sliceH = bottom - top;
|
const sliceH = bottom - top;
|
||||||
const sliceMm = sliceH / ratio;
|
|
||||||
|
|
||||||
const sc = document.createElement('canvas');
|
const sc = document.createElement('canvas');
|
||||||
sc.width = canvas.width;
|
sc.width = canvas.width;
|
||||||
sc.height = Math.ceil(sliceH);
|
sc.height = fullPageCanvasHeight;
|
||||||
sc.getContext('2d').drawImage(canvas, 0, top, canvas.width, sliceH, 0, 0, canvas.width, sliceH);
|
const sctx = sc.getContext('2d');
|
||||||
|
sctx.fillStyle = '#ffffff';
|
||||||
|
sctx.fillRect(0, 0, sc.width, sc.height);
|
||||||
|
sctx.drawImage(canvas, 0, top, canvas.width, sliceH, 0, 0, canvas.width, sliceH);
|
||||||
|
sctx.fillStyle = '#000000';
|
||||||
|
sctx.font = 'bold 24px "SimHei","黑体",sans-serif';
|
||||||
|
sctx.textAlign = 'center';
|
||||||
|
sctx.textBaseline = 'bottom';
|
||||||
|
sctx.fillText(`第 ${pageNum} 页,共 ${totalPages} 页`, sc.width / 2, sc.height - 4);
|
||||||
|
|
||||||
pdf.addImage(sc.toDataURL('image/jpeg', 0.95), 'JPEG', mg, mg, printW, sliceMm);
|
pdf.addImage(sc.toDataURL('image/jpeg', 0.95), 'JPEG', mg, mg, printW, printH);
|
||||||
|
|
||||||
if (i < pages.length - 1) {
|
if (i < pages.length - 1) {
|
||||||
pdf.addPage();
|
pdf.addPage();
|
||||||
@@ -459,22 +475,34 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const img = await this.loadImage(att.url);
|
const img = await this.loadImage(att.url);
|
||||||
pdf.addPage();
|
pdf.addPage();
|
||||||
|
pageNum++;
|
||||||
|
|
||||||
let drawW = img.width;
|
const canvasPageW = Math.round(printW * ratio);
|
||||||
let drawH = img.height;
|
const canvasImageH = Math.round(imagePrintH * ratio);
|
||||||
const scale = Math.min(printW / drawW, printH / drawH, 1);
|
const canvasFooterH = Math.round(footerMargin * ratio);
|
||||||
drawW *= scale;
|
const canvasTotalH = canvasImageH + canvasFooterH;
|
||||||
drawH *= scale;
|
|
||||||
|
|
||||||
const x = mg + (printW - drawW) / 2;
|
const attCanvas = document.createElement('canvas');
|
||||||
const y = mg + (printH - drawH) / 2;
|
attCanvas.width = canvasPageW;
|
||||||
|
attCanvas.height = canvasTotalH;
|
||||||
|
const actx = attCanvas.getContext('2d');
|
||||||
|
actx.fillStyle = '#ffffff';
|
||||||
|
actx.fillRect(0, 0, canvasPageW, canvasTotalH);
|
||||||
|
|
||||||
const tmpCanvas = document.createElement('canvas');
|
const imgScale = Math.min(canvasPageW / img.width, canvasImageH / img.height, 1);
|
||||||
tmpCanvas.width = img.width;
|
const imgDrawW = img.width * imgScale;
|
||||||
tmpCanvas.height = img.height;
|
const imgDrawH = img.height * imgScale;
|
||||||
tmpCanvas.getContext('2d').drawImage(img, 0, 0);
|
const imgX = (canvasPageW - imgDrawW) / 2;
|
||||||
|
const imgY = (canvasImageH - imgDrawH) / 2;
|
||||||
|
actx.drawImage(img, imgX, imgY, imgDrawW, imgDrawH);
|
||||||
|
|
||||||
pdf.addImage(tmpCanvas.toDataURL('image/jpeg', 0.95), 'JPEG', x, y, drawW, drawH);
|
actx.fillStyle = '#000000';
|
||||||
|
actx.font = 'bold 24px "SimHei","黑体",sans-serif';
|
||||||
|
actx.textAlign = 'center';
|
||||||
|
actx.textBaseline = 'bottom';
|
||||||
|
actx.fillText(`第 ${pageNum} 页,共 ${totalPages} 页`, canvasPageW / 2, canvasTotalH - 4);
|
||||||
|
|
||||||
|
pdf.addImage(attCanvas.toDataURL('image/jpeg', 0.95), 'JPEG', mg, mg, printW, printH);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载附件图片失败: ' + att.originalName, e);
|
console.error('加载附件图片失败: ' + att.originalName, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
<h3 style="margin-bottom: 20px; color: #303133; display: flex; justify-content: space-between; align-items: center;">
|
<h3 style="margin-bottom: 20px; color: #303133; display: flex; justify-content: space-between; align-items: center;">
|
||||||
{{ contract.contractName }}
|
{{ contract.contractName }}
|
||||||
<span>
|
<span>
|
||||||
<!-- <el-button type="primary" icon="el-icon-printer" @click="printContract">打印预览</el-button> -->
|
|
||||||
<el-select v-hasPermi="['crm:contract:status']" v-model="contract.status" placeholder="请选择合同状态" style="width: 150px; margin-left: 10px;" @change="handleStatusChange">
|
<el-select v-hasPermi="['crm:contract:status']" v-model="contract.status" placeholder="请选择合同状态" style="width: 150px; margin-left: 10px;" @change="handleStatusChange">
|
||||||
<el-option label="草稿" :value="0" />
|
<el-option label="草稿" :value="0" />
|
||||||
<el-option label="已生效" :value="1" />
|
<el-option label="已生效" :value="1" />
|
||||||
@@ -65,8 +64,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import ProductContent from './ProductContent.vue';
|
import ProductContent from './ProductContent.vue';
|
||||||
import OrderDetail from './OrderDetail.vue';
|
import OrderDetail from './OrderDetail.vue';
|
||||||
import { parseProductContent, convertToChinese } from '@/utils/productContent';
|
|
||||||
import contractLogo from '@/assets/images/contractLogo.png';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ContractPreview",
|
name: "ContractPreview",
|
||||||
@@ -109,176 +106,6 @@ export default {
|
|||||||
.replace('{i}', minutes)
|
.replace('{i}', minutes)
|
||||||
.replace('{s}', seconds);
|
.replace('{s}', seconds);
|
||||||
},
|
},
|
||||||
/** 打印预览(A4尺寸,多页支持) */
|
|
||||||
printContract() {
|
|
||||||
const row = this.contract;
|
|
||||||
if (!row) return;
|
|
||||||
|
|
||||||
let productData = parseProductContent(row.productContent);
|
|
||||||
if (!productData.productName && row.productName) {
|
|
||||||
productData.productName = row.productName;
|
|
||||||
}
|
|
||||||
const products = productData.products && productData.products.length > 0 ? productData.products : [];
|
|
||||||
const totalQty = products.reduce((a, p) => a + (parseFloat(p.quantity) || 0), 0);
|
|
||||||
const totalTax = products.reduce((a, p) => a + (parseFloat(p.taxTotal) || 0), 0);
|
|
||||||
const totalNoTax = products.reduce((a, p) => a + (parseFloat(p.noTaxTotal) || 0), 0);
|
|
||||||
const totalTaxAmt = products.reduce((a, p) => a + (parseFloat(p.taxAmount) || 0), 0);
|
|
||||||
const amountWords = productData.totalAmountInWords || convertToChinese(totalTax) || '零元整';
|
|
||||||
|
|
||||||
let productRowsHtml = '';
|
|
||||||
products.forEach((product, index) => {
|
|
||||||
productRowsHtml += `<tr>
|
|
||||||
<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${index + 1}</td>
|
|
||||||
<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${product.spec || ''}</td>
|
|
||||||
<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${product.material || ''}</td>
|
|
||||||
<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${product.quantity || ''}</td>
|
|
||||||
<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${product.taxPrice || ''}</td>
|
|
||||||
<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${product.taxDivisor || '1.13'}</td>
|
|
||||||
<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${(product.noTaxPrice || 0).toFixed(2)}</td>
|
|
||||||
<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${(product.taxTotal || 0).toFixed(2)}</td>
|
|
||||||
<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${(product.noTaxTotal || 0).toFixed(2)}</td>
|
|
||||||
<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${(product.taxAmount || 0).toFixed(2)}</td>
|
|
||||||
<td style="border:1px solid #000;padding:3px 4px;text-align:center;max-width:100px;word-wrap:break-word;">${product.remark || ''}</td>
|
|
||||||
</tr>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
let contractContentHtml = '';
|
|
||||||
if (row.contractContent) {
|
|
||||||
let htmlContent = row.contractContent;
|
|
||||||
const pTagRegex = /<p[^>]*>([\s\S]*?)<\/p>/g;
|
|
||||||
let match;
|
|
||||||
const pContents = [];
|
|
||||||
while ((match = pTagRegex.exec(htmlContent)) !== null) {
|
|
||||||
let content = match[1].replace(/<[^>]*>/g, '').replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'").trim();
|
|
||||||
if (content) {
|
|
||||||
const chineseNumberRegex = /^[一二三四五六七八九十]+、/;
|
|
||||||
pContents.push(chineseNumberRegex.test(content) ? content : ' ' + content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pContents.length === 0) {
|
|
||||||
const textContent = htmlContent.replace(/<[^>]*>/g, '').replace(/ /g, ' ').trim();
|
|
||||||
if (textContent) pContents.push(textContent);
|
|
||||||
}
|
|
||||||
if (pContents.length > 0) {
|
|
||||||
contractContentHtml = '<div style="margin-top:10px;font-size:12px;line-height:1.8;">' + pContents.join('<br/>') + '</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const printHtml = `<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head><meta charset="utf-8"><title>合同打印预览</title>
|
|
||||||
<style>
|
|
||||||
@page { size: A4; margin: 15mm; }
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
body { font-family: 'SimHei','黑体',sans-serif; font-weight: 600; color: #000; background: #e8e8e8; font-size: 12px; line-height: 1.6; display: flex; flex-direction: column; align-items: center; padding: 20px 0; }
|
|
||||||
.a4-page { width: 794px; min-height: 1123px; padding: 30px 40px; background: #fff; margin-bottom: 20px; box-shadow: 0 2px 12px rgba(0,0,0,0.15); }
|
|
||||||
.company-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 6px; }
|
|
||||||
.contract-title { text-align: center; font-size: 20px; font-weight: bold; letter-spacing: 6px; margin-bottom: 14px; }
|
|
||||||
.info-row { display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 13px; line-height: 2; }
|
|
||||||
.section-title { font-size: 13px; font-weight: bold; margin-bottom: 4px; margin-top: 8px; }
|
|
||||||
table { width: 100%; border-collapse: separate; border-spacing: 0; border-top: 2px solid #000; border-left: 2px solid #000; }
|
|
||||||
th, td { border-right: 2px solid #000; border-bottom: 2px solid #000; padding: 3px 4px; text-align: center; font-size: 11px; }
|
|
||||||
th { font-weight: 700; }
|
|
||||||
.sign-section { margin-top: 24px; font-size: 12px; line-height: 2.2; }
|
|
||||||
.sign-section .col { width: 48%; }
|
|
||||||
.sign-row { display: flex; justify-content: space-between; }
|
|
||||||
.print-toolbar { position: fixed; top: 0; right: 20px; z-index: 100; display: flex; gap: 8px; padding: 10px; }
|
|
||||||
.print-toolbar button { padding: 8px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
|
|
||||||
.print-toolbar .btn-print { background: #409eff; color: #fff; }
|
|
||||||
.print-toolbar .btn-print:hover { background: #66b1ff; }
|
|
||||||
@media print {
|
|
||||||
body { background: #fff; padding: 0; }
|
|
||||||
.a4-page { width: auto; min-height: auto; padding: 0; margin: 0; box-shadow: none; page-break-after: always; }
|
|
||||||
.print-toolbar { display: none !important; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="print-toolbar"><button class="btn-print" onclick="window.print()">打 印</button></div>
|
|
||||||
<div class="a4-page" style="position:relative;">
|
|
||||||
<img src="${contractLogo}" style="position:absolute;left:40px;top:40px;height:80px;" crossorigin="anonymous" />
|
|
||||||
<div style="text-align:center;padding-top:10px;">
|
|
||||||
<div style="font-size:20px;font-weight:bold;letter-spacing:2px;">嘉祥科伦普重工有限公司</div>
|
|
||||||
</div>
|
|
||||||
<div style="position:relative;margin:12px 0 8px 0;text-align:center;">
|
|
||||||
<div style="font-size:22px;font-weight:bold;letter-spacing:6px;">产 品 销 售 合 同</div>
|
|
||||||
<div style="position:absolute;right:0;top:50%;transform:translateY(-50%);font-size:11px;">合同编号:${row.contractCode || ''}</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-row">
|
|
||||||
<div style="width:55%;">
|
|
||||||
<div>供方(甲方):${row.supplier || ''}</div>
|
|
||||||
<div>需方(乙方):${row.customer || ''}</div>
|
|
||||||
</div>
|
|
||||||
<div style="width:40%;">
|
|
||||||
<div>签订时间:${row.signTime ? this.parseTime(row.signTime, '{y}年{m}月{d}日') : ''}</div>
|
|
||||||
<div>签订地点:${row.signLocation || ''}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="section-title">一、产品内容</div>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" style="font-weight:bold;text-align:left;">产品名称:${productData.productName || ''}</td>
|
|
||||||
<td colspan="7" style="font-weight:bold;text-align:left;">生产厂家:${row.manufacturer || '嘉祥科伦普重工有限公司'}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th style="width:30px;">序号</th>
|
|
||||||
<th style="width:80px;">规格(mm)</th>
|
|
||||||
<th style="width:55px;">材质</th>
|
|
||||||
<th style="width:50px;">数量(吨)</th>
|
|
||||||
<th style="width:70px;">含税单价(元/吨)</th>
|
|
||||||
<th style="width:45px;">税率除数</th>
|
|
||||||
<th style="width:70px;">无税单价(元/吨)</th>
|
|
||||||
<th style="width:70px;">含税总额(元)</th>
|
|
||||||
<th style="width:70px;">无税总额(元)</th>
|
|
||||||
<th style="width:50px;">税额(元)</th>
|
|
||||||
<th style="width:80px;">备注</th>
|
|
||||||
</tr>
|
|
||||||
${productRowsHtml}
|
|
||||||
<tr class="total-row">
|
|
||||||
<td colspan="4" style="text-align:center;">合 计</td>
|
|
||||||
<td>${totalQty.toFixed(2)}</td>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
<td>${totalTax.toFixed(2)}</td>
|
|
||||||
<td>${totalNoTax.toFixed(2)}</td>
|
|
||||||
<td>${totalTaxAmt.toFixed(2)}</td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
<tr><td colspan="11" style="font-weight:bold;text-align:left;">合计人民币(大写):${amountWords}</td></tr>
|
|
||||||
</table>
|
|
||||||
${contractContentHtml}
|
|
||||||
<div class="sign-section">
|
|
||||||
<div class="sign-row">
|
|
||||||
<div class="col">
|
|
||||||
<div>供方(甲方):${row.supplier || ''}</div>
|
|
||||||
<div>地址:${row.supplierAddress || ''}</div>
|
|
||||||
<div>电话:${row.supplierPhone || ''}</div>
|
|
||||||
<div>开户行:${row.supplierBank || ''}</div>
|
|
||||||
<div>账号:${row.supplierAccount || ''}</div>
|
|
||||||
<div>税号:${row.supplierTaxNo || ''}</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<div>需方(乙方):${row.customer || ''}</div>
|
|
||||||
<div>地址:${row.customerAddress || ''}</div>
|
|
||||||
<div>电话:${row.customerPhone || ''}</div>
|
|
||||||
<div>开户行:${row.customerBank || ''}</div>
|
|
||||||
<div>账号:${row.customerAccount || ''}</div>
|
|
||||||
<div>税号:${row.customerTaxNo || ''}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
|
|
||||||
const printWindow = window.open('', '_blank');
|
|
||||||
if (printWindow) {
|
|
||||||
printWindow.document.write(printHtml);
|
|
||||||
printWindow.document.close();
|
|
||||||
} else {
|
|
||||||
this.$message.warning('请允许浏览器弹出新窗口,或手动使用 Ctrl+P 打印');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleStatusChange(value) {
|
handleStatusChange(value) {
|
||||||
this.$emit('updateStatus', value);
|
this.$emit('updateStatus', value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,9 +299,9 @@ export default {
|
|||||||
getRowClassName({ row, rowIndex }) {
|
getRowClassName({ row, rowIndex }) {
|
||||||
const { rows } = this.highlightConfig
|
const { rows } = this.highlightConfig
|
||||||
|
|
||||||
if (this.isLengthAbnormal(row) || this.isThicknessAbnormal(row)) {
|
// if (this.isLengthAbnormal(row) || this.isThicknessAbnormal(row)) {
|
||||||
return 'warning-row'
|
// return 'warning-row'
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (!rows || (Array.isArray(rows) && rows.length === 0)) {
|
if (!rows || (Array.isArray(rows) && rows.length === 0)) {
|
||||||
return ''
|
return ''
|
||||||
|
|||||||
Reference in New Issue
Block a user