feat(contract): 新增合同打印功能,重构产品金额计算逻辑

1. 新增合同打印预览功能,支持A4格式打印,包含合同完整信息和产品明细
2. 新增多个产品金额计算工具函数,统一管理产品税额、无税单价等字段计算
3. 重构产品内容组件,新增税率除数、无税单价、税额列,实现字段联动自动计算
4. 新增合同logo静态资源,优化表格布局和样式
This commit is contained in:
2026-06-02 08:56:31 +08:00
parent a425a9052a
commit 6b8eac4139
6 changed files with 799 additions and 531 deletions

View File

@@ -3,12 +3,15 @@
<div v-if="contract" class="preview-content">
<h3 style="margin-bottom: 20px; color: #303133; display: flex; justify-content: space-between; align-items: center;">
{{ contract.contractName }}
<el-select v-hasPermi="['crm:contract:status']" v-model="contract.status" placeholder="请选择合同状态" style="width: 150px;" @change="handleStatusChange">
<el-option label="草稿" :value="0" />
<el-option label="已生效" :value="1" />
<el-option label="已取消" :value="2" />
<el-option label="已结清" :value="3" />
</el-select>
<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-option label="草稿" :value="0" />
<el-option label="已生效" :value="1" />
<el-option label="已取消" :value="2" />
<el-option label="已结清" :value="3" />
</el-select>
</span>
</h3>
<el-descriptions :column="2" border>
<el-descriptions-item label="合同编号">{{ contract.contractCode }}</el-descriptions-item>
@@ -62,6 +65,8 @@
<script>
import ProductContent from './ProductContent.vue';
import OrderDetail from './OrderDetail.vue';
import { parseProductContent, convertToChinese } from '@/utils/productContent';
import contractLogo from '@/assets/images/contractLogo.png';
export default {
name: "ContractPreview",
@@ -104,6 +109,177 @@ export default {
.replace('{i}', minutes)
.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(/&nbsp;/g, ' ').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
if (content) {
const chineseNumberRegex = /^[一二三四五六七八九十]+、/;
pContents.push(chineseNumberRegex.test(content) ? content : ' ' + content);
}
}
if (pContents.length === 0) {
const textContent = htmlContent.replace(/<[^>]*>/g, '').replace(/&nbsp;/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: 'SimSun','宋体',serif; 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: 8px; }
.contract-title { text-align: center; font-size: 22px; font-weight: bold; letter-spacing: 6px; margin-bottom: 18px; }
.info-row { display: flex; justify-content: space-between; margin-bottom: 6px; font-size: 13px; line-height: 2; }
.section-title { font-size: 13px; font-weight: bold; margin-bottom: 4px; margin-top: 10px; }
table { width: 100%; border-collapse: collapse; font-size: 11px; }
th, td { border: 1px solid #000; padding: 3px 4px; text-align: center; }
th { background-color: #f5f5f5; font-weight: bold; }
.total-row td { font-weight: bold; }
.sign-section { margin-top: 30px; 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:20px;top:20px;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="display:flex;justify-content:center;align-items:baseline;margin:12px 0 8px 0;">
<div style="font-size:22px;font-weight:bold;letter-spacing:6px;">产 品 销 售 合 同</div>
<div style="font-size:11px;margin-left:40px;">合同编号:${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;">合&nbsp;&nbsp;计</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) {
this.$emit('updateStatus', value);
}