Files
klp-oa/klp-ui/src/views/crm/contract/components/ProductContent.vue
砂糖 20cefe115d feat(金额显示): 统一金额单位为万元并优化产品内容处理
refactor(产品内容): 提取产品内容处理逻辑到独立工具类
style(合同表单): 调整技术附件和商务附件顺序
2026-05-11 10:38:29 +08:00

375 lines
9.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<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-input style="width: 50%" v-model="productName" placeholder="请输入产品名称" size="small" :readonly="readonly"/></div>
</div>
<div class="table-cell" colspan="5">
<div class="company-name">生产厂家嘉祥科伦普重工有限公司</div>
</div>
</div>
<!-- 第二行为表头 -->
<div class="table-row">
<div class="table-cell">序号</div>
<div class="table-cell">规格mm</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">
备注
<el-button
v-if="!readonly"
type="primary"
size="mini"
icon="el-icon-plus"
@click="addProduct"
style="margin-left: 10px;"
>
添加行
</el-button>
</div>
</div>
<!-- 产品行 -->
<div
v-for="(item, index) in products"
:key="index"
class="table-row"
:class="{ 'table-row-hover': !readonly }"
>
<div class="table-cell">
<div class="serial-number">
<span>{{ index + 1 }}</span>
<el-button
v-if="!readonly && products.length > 1"
type="text"
size="mini"
icon="el-icon-delete"
class="delete-btn"
@click="removeProduct(index)"
/>
</div>
</div>
<div class="table-cell">
<el-input
v-model="item.spec"
placeholder="请输入规格"
:readonly="readonly"
size="small"
/>
</div>
<div class="table-cell">
<el-input
v-model="item.material"
placeholder="请输入材质"
:readonly="readonly"
size="small"
/>
</div>
<div class="table-cell">
<el-input
v-model.number="item.quantity"
placeholder="请输入数量"
type="number"
:readonly="readonly"
size="small"
@change="calculateTotals"
/>
</div>
<div class="table-cell" v-if="false">
<el-input
v-model.number="item.taxPrice"
placeholder="请输入含税单价"
type="number"
:readonly="readonly"
size="small"
@change="calculateTotals"
/>
</div>
<div class="table-cell">
<el-input
v-model.number="item.noTaxPrice"
placeholder="请输入不含税单价"
type="number"
:readonly="readonly"
size="small"
@change="calculateTotals"
/>
</div>
<div class="table-cell">
<el-input
v-model.number="item.taxTotal"
placeholder="含税总额"
type="number"
:readonly="true"
size="small"
/>
</div>
<div class="table-cell">
<el-input
:value="((item.taxTotal || 0) / 1.13).toFixed(2)"
placeholder="不含税总额"
type="number"
:readonly="true"
size="small"
/>
</div>
<div class="table-cell">
<el-input
v-model="item.remark"
placeholder="请输入备注"
:readonly="readonly"
size="small"
/>
</div>
</div>
<!-- 合计行 -->
<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"></div>
<div class="table-cell">{{ totalTaxTotal }}</div>
<div class="table-cell">{{ ((totalTaxTotal || 0) / 1.13).toFixed(2) }}</div>
<div class="table-cell"></div>
</div>
<!-- 合计人民币(大写) -->
<div class="table-row">
<div class="table-cell" colspan="8">
<span>合计人民币(大写)</span>
<span>{{ totalAmountInWords }}</span>
</div>
</div>
<!-- 备注 -->
<div class="table-row">
<div class="table-cell" colspan="8">
<span>备注</span>
<el-input
v-model="remark"
type="textarea"
placeholder="请输入备注"
:readonly="readonly"
:autosize="{ minRows: 2 }"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import {
parseProductContent,
calculateTotalQuantity,
calculateTotalTaxTotal,
calculateProductTaxTotal,
convertToChinese
} from '@/utils/productContent';
export default {
name: 'ProductContent',
props: {
value: {
// 一个json字符串
type: String,
required: true,
},
readonly: {
type: Boolean,
default: false
}
},
data() {
return {
loading: false,
products: [],
remark: '',
productName: '',
}
},
computed: {
// 计算总数量
totalQuantity() {
return calculateTotalQuantity(this.products);
},
// 计算总含税总额
totalTaxTotal() {
return calculateTotalTaxTotal(this.products);
},
// 计算大写金额
totalAmountInWords() {
return convertToChinese(this.totalTaxTotal);
},
// 生成JSON字符串
jsonContent() {
// 只存储非空行
const nonEmptyProducts = this.products.filter(item => {
return item.spec || item.material || item.quantity || item.taxPrice || item.noTaxPrice || item.remark;
});
const data = {
products: nonEmptyProducts,
productName: this.productName,
remark: this.remark,
totalQuantity: this.totalQuantity,
totalTaxTotal: this.totalTaxTotal,
totalAmountInWords: this.totalAmountInWords
};
return JSON.stringify(data, null, 2);
},
},
watch: {
// 监听jsonContent变化触发update事件
jsonContent: {
handler(newValue) {
this.$emit('input', newValue);
},
deep: true
},
// 监听value变化更新内部数据
value: {
handler(newValue) {
if (!newValue) {
this.products = [{}];
this.remark = '';
this.productName = '';
return;
}
this.parseContent(newValue);
},
immediate: true
}
},
methods: {
// 解析content字符串
parseContent(content) {
try {
const data = parseProductContent(content);
this.products = data.products.length > 0 ? data.products : [{}];
this.remark = data.remark || '';
this.productName = data.productName || '';
} catch (error) {
console.error('解析content失败:', error);
this.products = [{}];
this.remark = '';
}
},
// 计算金额
calculateTotals() {
this.products.forEach(item => {
item.taxTotal = calculateProductTaxTotal(item);
});
},
// 添加产品行
addProduct() {
this.products.push({});
},
// 删除产品行
removeProduct(index) {
if (this.products.length > 1) {
this.products.splice(index, 1);
this.calculateTotals();
}
}
},
mounted() {
// 确保至少有一个空行
if (this.products.length === 0) {
this.products.push({});
}
}
}
</script>
<style scoped>
.product-content {
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
}
.table-header {
background-color: #f5f7fa;
text-align: center;
border-bottom: 1px solid #e4e7ed;
}
.company-name {
font-size: 16px;
font-weight: bold;
}
.table-row {
display: grid;
grid-template-columns: 80px 150px 120px 100px 160px 120px 140px 1fr;
border-bottom: 1px solid #e4e7ed;
}
.table-row-hover:hover {
background-color: #f5f7fa;
}
.table-header-row {
background-color: #f5f7fa;
font-weight: bold;
}
.table-total-row {
background-color: #f5f7fa;
font-weight: bold;
}
.table-cell {
padding: 8px;
border-right: 1px solid #e4e7ed;
display: flex;
align-items: center;
}
.table-cell:last-child {
border-right: none;
}
.table-cell[colspan="3"] {
grid-column: span 3;
}
.table-cell[colspan="5"] {
grid-column: span 5;
}
.table-cell[colspan="8"] {
grid-column: span 8;
}
.table-cell[colspan="9"] {
grid-column: span 9;
}
.serial-number {
position: relative;
display: inline-flex;
align-items: center;
}
.delete-btn {
opacity: 0;
margin-left: 10px;
}
.serial-number:hover .delete-btn {
opacity: 1;
}
.el-input {
width: 100%;
}
</style>