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

@@ -2,16 +2,16 @@
<div v-loading="loading">
<!-- 网格布局实现的表格共8列 -->
<div class="product-content">
<!-- 第一行合并所有八个单元格内容为嘉祥科伦普重工有限公司 -->
<!-- 第一行合并所有单元格 -->
<div class="table-row table-header">
<div class="table-cell" colspan="3">
<div class="company-name">产品名称
<el-select v-model="productName" placeholder="请选择产品名称" size="small" :readonly="readonly" filterable allow-create clearable>
<el-select style="width: 120px;" v-model="productName" placeholder="请选择产品名称" size="small" :readonly="readonly" filterable allow-create clearable>
<el-option v-for="item in productOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
</div>
<div class="table-cell" colspan="5">
<div class="table-cell" colspan="8">
<div class="company-name">生产厂家嘉祥科伦普重工有限公司</div>
</div>
</div>
@@ -23,9 +23,11 @@
<div class="table-cell">材质</div>
<div class="table-cell">数量</div>
<div class="table-cell">含税单价/</div>
<div class="table-cell" v-if="false">不含税单价/</div>
<div class="table-cell">税率除数</div>
<div class="table-cell">无税单价/</div>
<div class="table-cell">含税总额</div>
<div class="table-cell">不含税总额</div>
<div class="table-cell">税总额</div>
<div class="table-cell">税额</div>
<div class="table-cell">
备注
<el-button v-if="!readonly" type="primary" size="mini" icon="el-icon-plus" @click="addProduct"
@@ -52,23 +54,32 @@
</div>
<div class="table-cell">
<el-input v-model.number="item.quantity" placeholder="请输入数量" type="number" :readonly="readonly" size="small"
@change="calculateTotals" />
@change="onQuantityChange(item)" />
</div>
<div class="table-cell">
<el-input v-model.number="item.taxPrice" placeholder="请输入含税单价" type="number" :readonly="readonly" size="small"
@change="calculateTotals" />
</div>
<div class="table-cell" v-if="false">
<el-input v-model.number="item.noTaxPrice" placeholder="请输入不含税单价" type="number" :readonly="readonly"
size="small" @change="calculateTotals" />
@change="onTaxPriceChange(item)" />
</div>
<div class="table-cell">
<el-input v-model.number="item.taxTotal" placeholder="含税总额" type="number" :readonly="true" size="small" />
<el-input v-model.number="item.taxDivisor" placeholder="税率除数" type="number" :readonly="readonly"
size="small" @change="onTaxDivisorChange(item)" />
</div>
<div class="table-cell">
<el-input :value="((item.taxTotal || 0) / 1.13).toFixed(2)" placeholder="不含税总额" type="number" :readonly="true"
size="small" />
<el-input v-model.number="item.noTaxPrice" placeholder="无税单价" type="number" :readonly="readonly"
size="small" @change="onNoTaxPriceChange(item)" />
</div>
<div class="table-cell">
<el-input v-model.number="item.taxTotal" placeholder="含税总额" type="number" :readonly="readonly" size="small"
@change="onTaxTotalChange(item)" />
</div>
<div class="table-cell">
<el-input v-model.number="item.noTaxTotal" placeholder="无税总额" type="number" :readonly="readonly"
size="small" @change="onNoTaxTotalChange(item)" />
</div>
<div class="table-cell">
<el-input v-model.number="item.taxAmount" placeholder="税额" type="number" :readonly="readonly" size="small"
@change="onTaxAmountChange(item)" />
</div>
<div class="table-cell">
<el-input v-model="item.remark" placeholder="请输入备注" :readonly="readonly" size="small" />
@@ -78,17 +89,19 @@
<!-- 合计行 -->
<div class="table-row table-total-row">
<div class="table-cell" colspan="3">合计</div>
<div class="table-cell">{{ totalQuantity }}</div>
<div class="table-cell" v-if="false"></div>
<div class="table-cell">{{ totalQuantity.toFixed(2) }}</div>
<div class="table-cell"></div>
<div class="table-cell">{{ totalTaxTotal }}</div>
<div class="table-cell">{{ ((totalTaxTotal || 0) / 1.13).toFixed(2) }}</div>
<div class="table-cell"></div>
<div class="table-cell"></div>
<div class="table-cell">{{ totalTaxTotal.toFixed(2) }}</div>
<div class="table-cell">{{ totalNoTaxTotal.toFixed(2) }}</div>
<div class="table-cell">{{ totalTaxAmount.toFixed(2) }}</div>
<div class="table-cell"></div>
</div>
<!-- 合计人民币(大写) -->
<div class="table-row">
<div class="table-cell" colspan="8">
<div class="table-cell" colspan="11">
<span>合计人民币(大写)</span>
<span>{{ totalAmountInWords }}</span>
</div>
@@ -96,7 +109,7 @@
<!-- 备注 -->
<div class="table-row">
<div class="table-cell" colspan="8">
<div class="table-cell" colspan="11">
<span>备注</span>
<el-input v-model="remark" type="textarea" placeholder="请输入备注" :readonly="readonly"
:autosize="{ minRows: 2 }" />
@@ -111,7 +124,7 @@ import {
parseProductContent,
calculateTotalQuantity,
calculateTotalTaxTotal,
calculateProductTaxTotal,
calculateProductFields,
convertToChinese
} from '@/utils/productContent';
@@ -151,6 +164,18 @@ export default {
totalTaxTotal() {
return calculateTotalTaxTotal(this.products);
},
// 计算总无税总额
totalNoTaxTotal() {
return this.products.reduce((total, item) => {
return total + (parseFloat(item.noTaxTotal) || 0);
}, 0);
},
// 计算总税额
totalTaxAmount() {
return this.products.reduce((total, item) => {
return total + (parseFloat(item.taxAmount) || 0);
}, 0);
},
// 计算大写金额
totalAmountInWords() {
return convertToChinese(this.totalTaxTotal);
@@ -159,7 +184,7 @@ export default {
jsonContent() {
// 只存储非空行
const nonEmptyProducts = this.products.filter(item => {
return item.spec || item.material || item.quantity || item.taxPrice || item.noTaxPrice || item.remark;
return item.spec || item.material || item.quantity || item.taxPrice || item.remark;
});
const data = {
products: nonEmptyProducts,
@@ -167,6 +192,8 @@ export default {
remark: this.remark,
totalQuantity: this.totalQuantity,
totalTaxTotal: this.totalTaxTotal,
totalNoTaxTotal: this.totalNoTaxTotal,
totalTaxAmount: this.totalTaxAmount,
totalAmountInWords: this.totalAmountInWords
};
return JSON.stringify(data, null, 2);
@@ -184,7 +211,9 @@ export default {
value: {
handler(newValue) {
if (!newValue) {
this.products = [{}];
const defaultItem = { taxDivisor: 1.13 };
this.initProduct(defaultItem);
this.products = [defaultItem];
this.remark = '';
this.productName = '';
return;
@@ -195,11 +224,26 @@ export default {
}
},
methods: {
// 初始化产品的默认字段
initProduct(item) {
if (item.taxDivisor === undefined || item.taxDivisor === null) {
item.taxDivisor = 1.13;
}
if (item.noTaxPrice === undefined) item.noTaxPrice = 0;
if (item.noTaxTotal === undefined) item.noTaxTotal = 0;
if (item.taxAmount === undefined) item.taxAmount = 0;
if (item.taxTotal === undefined) item.taxTotal = 0;
},
// 解析content字符串
parseContent(content) {
try {
const data = parseProductContent(content);
this.products = data.products.length > 0 ? data.products : [{}];
const products = data.products.length > 0 ? data.products : [{}];
products.forEach(item => {
this.initProduct(item);
Object.assign(item, calculateProductFields(item, 'quantity'));
});
this.products = products;
this.remark = data.remark || '';
this.productName = data.productName || '';
} catch (error) {
@@ -208,15 +252,45 @@ export default {
this.remark = '';
}
},
// 数量变更
onQuantityChange(item) {
Object.assign(item, calculateProductFields(item, 'quantity'));
},
// 含税单价变更
onTaxPriceChange(item) {
Object.assign(item, calculateProductFields(item, 'taxPrice'));
},
// 税率除数变更
onTaxDivisorChange(item) {
Object.assign(item, calculateProductFields(item, 'taxDivisor'));
},
// 无税单价变更
onNoTaxPriceChange(item) {
Object.assign(item, calculateProductFields(item, 'noTaxPrice'));
},
// 含税总额变更
onTaxTotalChange(item) {
Object.assign(item, calculateProductFields(item, 'taxTotal'));
},
// 无税总额变更
onNoTaxTotalChange(item) {
Object.assign(item, calculateProductFields(item, 'noTaxTotal'));
},
// 税额变更
onTaxAmountChange(item) {
Object.assign(item, calculateProductFields(item, 'taxAmount'));
},
// 计算金额
calculateTotals() {
this.products.forEach(item => {
item.taxTotal = calculateProductTaxTotal(item);
Object.assign(item, calculateProductFields(item, 'quantity'));
});
},
// 添加产品行
addProduct() {
this.products.push({});
const newItem = { taxDivisor: 1.13 };
this.initProduct(newItem);
this.products.push(newItem);
},
// 删除产品行
removeProduct(index) {
@@ -229,7 +303,9 @@ export default {
mounted() {
// 确保至少有一个空行
if (this.products.length === 0) {
this.products.push({});
const newItem = { taxDivisor: 1.13 };
this.initProduct(newItem);
this.products.push(newItem);
}
}
}
@@ -239,7 +315,7 @@ export default {
.product-content {
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
overflow-x: auto;
}
.table-header {
@@ -251,11 +327,14 @@ export default {
.company-name {
font-size: 16px;
font-weight: bold;
display: flex;
align-items: center;
}
.table-row {
display: grid;
grid-template-columns: 80px 150px 120px 100px 160px 120px 140px 1fr;
grid-template-columns: 45px 100px 90px 75px 100px 90px 100px 110px 110px 110px 1fr;
border-bottom: 1px solid #e4e7ed;
}
@@ -300,6 +379,10 @@ export default {
grid-column: span 9;
}
.table-cell[colspan="11"] {
grid-column: span 11;
}
.serial-number {
position: relative;
display: inline-flex;
@@ -318,4 +401,14 @@ export default {
.el-input {
width: 100%;
}
/* 隐藏 number 输入框的上下箭头 */
.el-input /deep/ input[type="number"]::-webkit-outer-spin-button,
.el-input /deep/ input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.el-input /deep/ input[type="number"] {
-moz-appearance: textfield;
}
</style>