- feat(crm/contract): 含税总额变化后自动填写订单总金额(可配置开关) - fix(crm/receive): 修复金额单位错误(万元→元);清理未使用导入 - fix(contract/product): 产品备注设置默认值 - chore: 移除已废弃的 OrderDashboard 组件和 finance/order 页面 - feat(wms/hrm): 新增考勤异常管理页面(attendanceAbnormal.vue) - chore: 移除 trae git 提交规则配置
393 lines
11 KiB
Vue
393 lines
11 KiB
Vue
<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-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="8">
|
||
<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">含税单价(元/吨)</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"
|
||
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="onQuantityChange(item)" />
|
||
</div>
|
||
|
||
<div class="table-cell">
|
||
<el-input v-model.number="item.taxPrice" placeholder="请输入含税单价" type="number" :readonly="readonly" size="small"
|
||
@change="onTaxPriceChange(item)" />
|
||
</div>
|
||
<div class="table-cell">
|
||
<el-input v-model.number="item.taxDivisor" placeholder="税率除数" type="number" :readonly="readonly"
|
||
size="small" />
|
||
</div>
|
||
<div class="table-cell">
|
||
<el-input v-model.number="item.noTaxPrice" placeholder="无税单价" type="number" :readonly="readonly"
|
||
size="small" />
|
||
</div>
|
||
<div class="table-cell">
|
||
<el-input v-model.number="item.taxTotal" placeholder="含税总额" type="number" :readonly="readonly" size="small" />
|
||
</div>
|
||
<div class="table-cell">
|
||
<el-input v-model.number="item.noTaxTotal" placeholder="无税总额" type="number" :readonly="readonly"
|
||
size="small" />
|
||
</div>
|
||
<div class="table-cell">
|
||
<el-input v-model.number="item.taxAmount" placeholder="税额" type="number" :readonly="readonly" 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.toFixed(3) }}</div>
|
||
<div class="table-cell"></div>
|
||
<div class="table-cell"></div>
|
||
<div class="table-cell"></div>
|
||
<div class="table-cell">{{ totalTaxTotal.toFixed(3) }}</div>
|
||
<div class="table-cell">{{ totalNoTaxTotal.toFixed(3) }}</div>
|
||
<div class="table-cell">{{ totalTaxAmount.toFixed(3) }}</div>
|
||
<div class="table-cell"></div>
|
||
</div>
|
||
|
||
<!-- 合计人民币(大写) -->
|
||
<div class="table-row">
|
||
<div class="table-cell" colspan="11">
|
||
<span>合计人民币(大写):</span>
|
||
<span>{{ totalAmountInWords }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 备注 -->
|
||
<div class="table-row">
|
||
<div class="table-cell" colspan="11">
|
||
<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,
|
||
calculateProductFields,
|
||
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: '',
|
||
productOptions: [
|
||
{ label: '冷硬钢卷', value: '冷硬钢卷' },
|
||
{ label: '镀锌钢卷', value: '镀锌钢卷' },
|
||
{ label: '冷轧钢卷', value: '冷轧钢卷' },
|
||
{ label: '镀铬钢卷', value: '镀铬钢卷' },
|
||
]
|
||
}
|
||
},
|
||
computed: {
|
||
// 计算总数量
|
||
totalQuantity() {
|
||
return calculateTotalQuantity(this.products);
|
||
},
|
||
// 计算总含税总额
|
||
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);
|
||
},
|
||
// 生成JSON字符串
|
||
jsonContent() {
|
||
// 只存储非空行
|
||
const nonEmptyProducts = this.products.filter(item => {
|
||
return item.spec || item.material || item.quantity || item.taxPrice || item.remark;
|
||
});
|
||
const data = {
|
||
products: nonEmptyProducts,
|
||
productName: this.productName,
|
||
remark: this.remark,
|
||
totalQuantity: this.totalQuantity,
|
||
totalTaxTotal: this.totalTaxTotal,
|
||
totalNoTaxTotal: this.totalNoTaxTotal,
|
||
totalTaxAmount: this.totalTaxAmount,
|
||
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) {
|
||
const defaultItem = { taxDivisor: 1.13 };
|
||
this.initProduct(defaultItem);
|
||
this.products = [defaultItem];
|
||
this.remark = '';
|
||
this.productName = '';
|
||
return;
|
||
}
|
||
this.parseContent(newValue);
|
||
},
|
||
immediate: true
|
||
}
|
||
},
|
||
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);
|
||
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) {
|
||
console.error('解析content失败:', error);
|
||
this.products = [{}];
|
||
this.remark = '净边料/毛边料、简包/裸包、卷重结算';
|
||
}
|
||
},
|
||
// 数量变更
|
||
onQuantityChange(item) {
|
||
Object.assign(item, calculateProductFields(item, 'quantity'));
|
||
},
|
||
// 含税单价变更
|
||
onTaxPriceChange(item) {
|
||
Object.assign(item, calculateProductFields(item, 'taxPrice'));
|
||
},
|
||
|
||
// 计算金额
|
||
calculateTotals() {
|
||
this.products.forEach(item => {
|
||
Object.assign(item, calculateProductFields(item, 'quantity'));
|
||
});
|
||
},
|
||
// 添加产品行
|
||
addProduct() {
|
||
const newItem = { taxDivisor: 1.13 };
|
||
this.initProduct(newItem);
|
||
this.products.push(newItem);
|
||
},
|
||
// 删除产品行
|
||
removeProduct(index) {
|
||
if (this.products.length > 1) {
|
||
this.products.splice(index, 1);
|
||
this.calculateTotals();
|
||
}
|
||
}
|
||
},
|
||
mounted() {
|
||
// 确保至少有一个空行
|
||
if (this.products.length === 0) {
|
||
const newItem = { taxDivisor: 1.13 };
|
||
this.initProduct(newItem);
|
||
this.products.push(newItem);
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.product-content {
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 4px;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.table-header {
|
||
background-color: #f5f7fa;
|
||
text-align: center;
|
||
border-bottom: 1px solid #e4e7ed;
|
||
}
|
||
|
||
.company-name {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
}
|
||
|
||
.table-row {
|
||
display: grid;
|
||
grid-template-columns: 45px 100px 90px 75px 100px 90px 100px 110px 110px 110px 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;
|
||
}
|
||
|
||
.table-cell[colspan="11"] {
|
||
grid-column: span 11;
|
||
}
|
||
|
||
.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%;
|
||
}
|
||
|
||
/* 隐藏 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> |