Merge remote-tracking branch 'origin/0.8.X' into 0.8.X
This commit is contained in:
BIN
klp-ui/src/assets/images/contractLogo.png
Normal file
BIN
klp-ui/src/assets/images/contractLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
@@ -89,6 +89,122 @@ export function calculateProductTaxTotal(product) {
|
||||
return quantity * taxPrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算无税单价 = 含税单价 / 税率除数
|
||||
* @param {Object} product - 产品对象
|
||||
* @returns {number} 无税单价
|
||||
*/
|
||||
export function calculateProductNoTaxPrice(product) {
|
||||
if (!product) return 0;
|
||||
const taxPrice = parseFloat(product.taxPrice) || 0;
|
||||
const taxDivisor = parseFloat(product.taxDivisor) || 1.13;
|
||||
return taxPrice / taxDivisor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算无税总额 = 数量 * 无税单价
|
||||
* @param {Object} product - 产品对象
|
||||
* @returns {number} 无税总额
|
||||
*/
|
||||
export function calculateProductNoTaxTotal(product) {
|
||||
if (!product) return 0;
|
||||
const quantity = parseFloat(product.quantity) || 0;
|
||||
const noTaxPrice = parseFloat(product.noTaxPrice) || 0;
|
||||
return quantity * noTaxPrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算税额 = 含税总额 - 无税总额
|
||||
* @param {Object} product - 产品对象
|
||||
* @returns {number} 税额
|
||||
*/
|
||||
export function calculateProductTaxAmount(product) {
|
||||
if (!product) return 0;
|
||||
const taxTotal = parseFloat(product.taxTotal) || 0;
|
||||
const noTaxTotal = parseFloat(product.noTaxTotal) || 0;
|
||||
return taxTotal - noTaxTotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变更字段重新计算产品所有金额字段
|
||||
* @param {Object} product - 产品对象
|
||||
* @param {string} changedField - 变更的字段名
|
||||
* @returns {Object} 更新后的产品对象
|
||||
*/
|
||||
export function calculateProductFields(product, changedField = 'quantity') {
|
||||
if (!product) return product;
|
||||
|
||||
const quantity = parseFloat(product.quantity) || 0;
|
||||
let taxPrice = parseFloat(product.taxPrice) || 0;
|
||||
let taxDivisor = parseFloat(product.taxDivisor) || 1.13;
|
||||
let noTaxPrice, noTaxTotal, taxTotal, taxAmount;
|
||||
|
||||
switch (changedField) {
|
||||
case 'quantity':
|
||||
case 'taxPrice':
|
||||
taxTotal = quantity * taxPrice;
|
||||
noTaxPrice = taxPrice / taxDivisor;
|
||||
noTaxTotal = quantity * noTaxPrice;
|
||||
taxAmount = taxTotal - noTaxTotal;
|
||||
break;
|
||||
case 'taxDivisor':
|
||||
noTaxPrice = taxPrice / taxDivisor;
|
||||
taxTotal = quantity * taxPrice;
|
||||
noTaxTotal = quantity * noTaxPrice;
|
||||
taxAmount = taxTotal - noTaxTotal;
|
||||
break;
|
||||
case 'noTaxPrice':
|
||||
noTaxPrice = parseFloat(product.noTaxPrice) || 0;
|
||||
taxPrice = noTaxPrice * taxDivisor;
|
||||
taxTotal = quantity * taxPrice;
|
||||
noTaxTotal = quantity * noTaxPrice;
|
||||
taxAmount = taxTotal - noTaxTotal;
|
||||
break;
|
||||
case 'noTaxTotal':
|
||||
noTaxTotal = parseFloat(product.noTaxTotal) || 0;
|
||||
noTaxPrice = quantity > 0 ? noTaxTotal / quantity : 0;
|
||||
taxPrice = noTaxPrice * taxDivisor;
|
||||
taxTotal = quantity * taxPrice;
|
||||
taxAmount = taxTotal - noTaxTotal;
|
||||
break;
|
||||
case 'taxTotal':
|
||||
taxTotal = parseFloat(product.taxTotal) || 0;
|
||||
taxPrice = quantity > 0 ? taxTotal / quantity : 0;
|
||||
noTaxPrice = taxPrice / taxDivisor;
|
||||
noTaxTotal = quantity * noTaxPrice;
|
||||
taxAmount = taxTotal - noTaxTotal;
|
||||
break;
|
||||
case 'taxAmount':
|
||||
taxAmount = parseFloat(product.taxAmount) || 0;
|
||||
taxTotal = quantity * taxPrice;
|
||||
noTaxTotal = taxTotal - taxAmount;
|
||||
noTaxPrice = quantity > 0 ? noTaxTotal / quantity : 0;
|
||||
taxPrice = quantity > 0 ? noTaxPrice * taxDivisor : 0;
|
||||
taxTotal = quantity * taxPrice;
|
||||
noTaxTotal = quantity * noTaxPrice;
|
||||
taxAmount = taxTotal - noTaxTotal;
|
||||
break;
|
||||
default:
|
||||
taxTotal = quantity * taxPrice;
|
||||
noTaxPrice = taxPrice / taxDivisor;
|
||||
noTaxTotal = quantity * noTaxPrice;
|
||||
taxAmount = taxTotal - noTaxTotal;
|
||||
}
|
||||
|
||||
const round2 = (v) => Math.round(v * 100) / 100;
|
||||
|
||||
return {
|
||||
...product,
|
||||
quantity,
|
||||
taxPrice: round2(taxPrice),
|
||||
noTaxPrice: round2(noTaxPrice),
|
||||
taxTotal: round2(taxTotal),
|
||||
noTaxTotal: round2(noTaxTotal),
|
||||
taxAmount: round2(taxAmount),
|
||||
taxDivisor
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新计算所有产品的含税总额和总计
|
||||
* @param {Object} content - 产品内容对象
|
||||
@@ -99,10 +215,9 @@ export function recalculateTotals(content) {
|
||||
return { ...DEFAULT_PRODUCT_CONTENT };
|
||||
}
|
||||
|
||||
const products = (content.products || []).map(product => ({
|
||||
...product,
|
||||
taxTotal: calculateProductTaxTotal(product)
|
||||
}));
|
||||
const products = (content.products || []).map(product =>
|
||||
calculateProductFields(product, 'quantity')
|
||||
);
|
||||
|
||||
const totalQuantity = calculateTotalQuantity(products);
|
||||
const totalTaxTotal = calculateTotalTaxTotal(products);
|
||||
@@ -124,15 +239,16 @@ export function recalculateTotals(content) {
|
||||
export function convertOrderItemToProduct(orderItem) {
|
||||
if (!orderItem) return {};
|
||||
|
||||
return {
|
||||
const product = {
|
||||
spec: orderItem.finishedProductSpec || '',
|
||||
material: orderItem.material || '',
|
||||
quantity: parseFloat(orderItem.weight) || 0,
|
||||
taxPrice: parseFloat(orderItem.contractPrice) || 0,
|
||||
noTaxPrice: parseFloat(orderItem.itemAmount) || 0,
|
||||
taxTotal: (parseFloat(orderItem.contractPrice) || 0) * (parseFloat(orderItem.weight) || 0),
|
||||
taxDivisor: parseFloat(orderItem.taxDivisor) || 1.13,
|
||||
remark: orderItem.remark || ''
|
||||
};
|
||||
|
||||
return calculateProductFields(product, 'quantity');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,6 +420,10 @@ export default {
|
||||
calculateTotalQuantity,
|
||||
calculateTotalTaxTotal,
|
||||
calculateProductTaxTotal,
|
||||
calculateProductNoTaxPrice,
|
||||
calculateProductNoTaxTotal,
|
||||
calculateProductTaxAmount,
|
||||
calculateProductFields,
|
||||
recalculateTotals,
|
||||
convertOrderItemToProduct,
|
||||
convertOrderItemsToContent,
|
||||
|
||||
@@ -133,13 +133,40 @@
|
||||
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" style="padding: 10px; margin-bottom: 10px !important;" />
|
||||
|
||||
<!-- 导出预览对话框 -->
|
||||
<el-dialog title="导出预览" :visible.sync="exportDialogVisible" width="95%" top="20px" append-to-body
|
||||
:close-on-click-modal="false" @open="generatePreviewHtml">
|
||||
<div style="display: flex; gap: 16px; height: calc(100vh - 200px);">
|
||||
<!-- 左侧:列配置 -->
|
||||
<div style="width: 200px; flex-shrink: 0; border: 1px solid #e4e7ed; border-radius: 4px; padding: 12px; overflow-y: auto;">
|
||||
<div style="font-weight: bold; margin-bottom: 10px; font-size: 14px;">产品表列配置</div>
|
||||
<el-checkbox v-model="selectAllColumns" :indeterminate="columnIndeterminate" @change="handleSelectAllColumns"
|
||||
style="margin-bottom: 8px;">全选</el-checkbox>
|
||||
<div v-for="col in columnConfigs" :key="col.key" style="margin-bottom: 6px;">
|
||||
<el-checkbox v-model="col.checked" @change="onColumnChange">{{ col.label }}</el-checkbox>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div style="font-size: 12px; color: #909399;">勾选的列将显示在导出的产品表中</div>
|
||||
</div>
|
||||
<!-- 右侧:预览 -->
|
||||
<div style="flex: 1; border: 1px solid #e4e7ed; border-radius: 4px; overflow: hidden; background: #f0f0f0;">
|
||||
<iframe ref="previewFrame" style="width: 100%; height: 100%; border: none; background: #fff;"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<span slot="footer">
|
||||
<el-button @click="exportDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" :loading="exportLoading" @click="confirmExport">确认导出</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listOrder, updateOrder } from "@/api/crm/order";
|
||||
import * as ExcelJS from 'exceljs';
|
||||
import { saveAs } from 'file-saver';
|
||||
import html2canvas from 'html2canvas';
|
||||
import jsPDF from 'jspdf';
|
||||
import contractLogo from '@/assets/images/contractLogo.png';
|
||||
import {
|
||||
parseProductContent,
|
||||
convertToChinese
|
||||
@@ -175,6 +202,24 @@ export default {
|
||||
signLocation: undefined,
|
||||
status: undefined,
|
||||
},
|
||||
// 导出预览
|
||||
exportDialogVisible: false,
|
||||
exportLoading: false,
|
||||
exportRow: null,
|
||||
selectAllColumns: true,
|
||||
columnIndeterminate: false,
|
||||
columnConfigs: [
|
||||
{ key: 'spec', label: '规格(mm)', checked: true },
|
||||
{ key: 'material', label: '材质', checked: true },
|
||||
{ key: 'quantity', label: '数量(吨)', checked: true },
|
||||
{ key: 'taxPrice', label: '含税单价(元/吨)', checked: true },
|
||||
{ key: 'taxDivisor', label: '税率除数', checked: true },
|
||||
{ key: 'noTaxPrice', label: '无税单价(元/吨)', checked: true },
|
||||
{ key: 'taxTotal', label: '含税总额(元)', checked: true },
|
||||
{ key: 'noTaxTotal', label: '无税总额(元)', checked: true },
|
||||
{ key: 'taxAmount', label: '税额(元)', checked: true },
|
||||
{ key: 'remark', label: '备注', checked: true },
|
||||
],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@@ -235,529 +280,359 @@ export default {
|
||||
toggleMoreFilter() {
|
||||
this.showMoreFilter = !this.showMoreFilter;
|
||||
},
|
||||
convertToChinese(amount) {
|
||||
return convertToChinese(amount);
|
||||
/** 导出合同 - 打开预览对话框 */
|
||||
handleExport(row) {
|
||||
this.exportRow = row;
|
||||
this.exportDialogVisible = true;
|
||||
},
|
||||
/** 导出合同 */
|
||||
async handleExport(row) {
|
||||
// 1. 创建excel
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet('产品销售合同');
|
||||
// let orderItems = [];
|
||||
// // 2. 查询合同详情
|
||||
// const res = await listOrderItem({ orderId: row.orderId, pageNum: 1, pageSize: 1000 });
|
||||
// orderItems = res.rows || [];
|
||||
/** 全选/取消全选 */
|
||||
handleSelectAllColumns(val) {
|
||||
this.columnConfigs.forEach(col => { col.checked = val; });
|
||||
this.columnIndeterminate = false;
|
||||
this.generatePreviewHtml();
|
||||
},
|
||||
/** 单个列勾选变化 */
|
||||
onColumnChange() {
|
||||
const checkedCount = this.columnConfigs.filter(c => c.checked).length;
|
||||
this.selectAllColumns = checkedCount === this.columnConfigs.length;
|
||||
this.columnIndeterminate = checkedCount > 0 && checkedCount < this.columnConfigs.length;
|
||||
this.generatePreviewHtml();
|
||||
},
|
||||
/** 根据列配置生成产品表HTMl */
|
||||
buildProductTableHtml(productData, products) {
|
||||
const activeCols = this.columnConfigs.filter(c => c.checked);
|
||||
const hasCol = (key) => activeCols.some(c => c.key === key);
|
||||
|
||||
// 2. 设置列宽
|
||||
worksheet.columns = [
|
||||
{ width: 10 },
|
||||
{ width: 20 },
|
||||
{ width: 15 },
|
||||
{ width: 15 },
|
||||
{ width: 15 },
|
||||
{ width: 15 },
|
||||
{ width: 15 },
|
||||
{ width: 20 }
|
||||
];
|
||||
// 表头
|
||||
let headerCells = '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:30px;">序号</th>';
|
||||
if (hasCol('spec')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:80px;">规格(mm)</th>';
|
||||
if (hasCol('material')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:60px;">材质</th>';
|
||||
if (hasCol('quantity')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:55px;">数量(吨)</th>';
|
||||
if (hasCol('taxPrice')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:70px;">含税单价(元/吨)</th>';
|
||||
if (hasCol('taxDivisor')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:45px;">税率除数</th>';
|
||||
if (hasCol('noTaxPrice')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:70px;">无税单价(元/吨)</th>';
|
||||
if (hasCol('taxTotal')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:70px;">含税总额(元)</th>';
|
||||
if (hasCol('noTaxTotal')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:70px;">无税总额(元)</th>';
|
||||
if (hasCol('taxAmount')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:55px;">税额(元)</th>';
|
||||
if (hasCol('remark')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:80px;">备注</th>';
|
||||
const colCount = activeCols.length + 1; // +1 for 序号
|
||||
|
||||
// 3. 合并单元格并设置内容
|
||||
// 公司信息
|
||||
worksheet.mergeCells('A1:H1');
|
||||
worksheet.getCell('A1').value = '嘉祥科伦普重工有限公司';
|
||||
worksheet.getCell('A1').font = { size: 16, bold: true };
|
||||
worksheet.getCell('A1').alignment = { horizontal: 'center', vertical: 'middle' };
|
||||
|
||||
// 合同标题
|
||||
worksheet.mergeCells('A2:F2');
|
||||
worksheet.getCell('A2').value = '产品销售合同';
|
||||
worksheet.getCell('A2').font = { size: 18, bold: true };
|
||||
worksheet.getCell('A2').alignment = { horizontal: 'center', vertical: 'middle' };
|
||||
|
||||
// 合同编号
|
||||
worksheet.mergeCells('G2:H2');
|
||||
worksheet.getCell('G2').value = `合同编号:${row.contractCode || ''}`;
|
||||
worksheet.getCell('G2').alignment = { horizontal: 'right', vertical: 'middle' };
|
||||
|
||||
// 供方信息
|
||||
worksheet.getCell('A3').value = `供方(甲方):${row.supplier || '嘉祥科伦普重工有限公司'}`;
|
||||
worksheet.getCell('A3').alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
|
||||
// 签订时间
|
||||
worksheet.getCell('E3').value = `签订时间:${row.signTime ? this.parseTime(row.signTime, '{y}年{m}月{d}日') : '2026年 月 日'}`;
|
||||
worksheet.getCell('E3').alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
|
||||
// 需方信息
|
||||
worksheet.getCell('A4').value = `需方(乙方):${row.customer || ''}`;
|
||||
worksheet.getCell('A4').alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
|
||||
// 签订地点
|
||||
worksheet.getCell('E4').value = `签订地点:${row.signLocation || '山东省济宁市嘉祥县'}`;
|
||||
worksheet.getCell('E4').alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
|
||||
// 产品内容标题
|
||||
worksheet.getCell('A5').value = '一、产品内容';
|
||||
worksheet.getCell('A5').font = { bold: true };
|
||||
|
||||
// 产品名称和生产厂家
|
||||
worksheet.mergeCells('A6:C6');
|
||||
worksheet.getCell('A6').value = `产品名称:${row.productName || '冷硬钢卷'}`;
|
||||
worksheet.getCell('A6').alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
worksheet.getCell('A6').border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
|
||||
|
||||
worksheet.mergeCells('D6:H6');
|
||||
worksheet.getCell('D6').value = `生产厂家:${row.manufacturer || '嘉祥科伦普重工有限公司'}`;
|
||||
worksheet.getCell('D6').alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
worksheet.getCell('D6').border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
|
||||
// 产品表格标题(分为两行)
|
||||
// 合并序号、规格、材质、备注列的单元格(两行)
|
||||
const mergedHeaders = ['序号', '规格(mm)', '材质', '备注'];
|
||||
const mergedColumns = [1, 2, 3, 8];
|
||||
|
||||
mergedColumns.forEach((col, index) => {
|
||||
// 合并两行
|
||||
worksheet.mergeCells(7, col, 8, col);
|
||||
const cell = worksheet.getCell(7, col);
|
||||
cell.value = mergedHeaders[index];
|
||||
cell.font = { bold: true };
|
||||
cell.fill = { type: 'pattern', pattern: 'solid' };
|
||||
cell.border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
cell.alignment = { horizontal: 'center', vertical: 'middle' };
|
||||
// 数据行
|
||||
let bodyRows = '';
|
||||
products.forEach((product, index) => {
|
||||
let cells = `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${index + 1}</td>`;
|
||||
if (hasCol('spec')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${product.spec || ''}</td>`;
|
||||
if (hasCol('material')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${product.material || ''}</td>`;
|
||||
if (hasCol('quantity')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${product.quantity || ''}</td>`;
|
||||
if (hasCol('taxPrice')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${product.taxPrice || ''}</td>`;
|
||||
if (hasCol('taxDivisor')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${product.taxDivisor || '1.13'}</td>`;
|
||||
if (hasCol('noTaxPrice')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${(product.noTaxPrice || 0).toFixed(2)}</td>`;
|
||||
if (hasCol('taxTotal')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${(product.taxTotal || 0).toFixed(2)}</td>`;
|
||||
if (hasCol('noTaxTotal')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${(product.noTaxTotal || 0).toFixed(2)}</td>`;
|
||||
if (hasCol('taxAmount')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${(product.taxAmount || 0).toFixed(2)}</td>`;
|
||||
if (hasCol('remark')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;max-width:100px;word-wrap:break-word;">${product.remark || ''}</td>`;
|
||||
bodyRows += `<tr>${cells}</tr>`;
|
||||
});
|
||||
|
||||
// 数量、含税单价、不含税单价、含税总额的标题和单位各占一行
|
||||
const quantityHeaders = ['数量', '(吨)'];
|
||||
const taxPriceHeaders = ['含税单价', '(元/吨)'];
|
||||
const noTaxPriceHeaders = ['不含税单价', '(元/吨)'];
|
||||
const taxTotalHeaders = ['含税总额', '(元)'];
|
||||
// 合计行
|
||||
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 totalAmountInWords = productData.totalAmountInWords || convertToChinese(totalTax) || '零元整';
|
||||
|
||||
// 数量列
|
||||
worksheet.getCell(7, 4).value = quantityHeaders[0];
|
||||
worksheet.getCell(8, 4).value = quantityHeaders[1];
|
||||
|
||||
// 含税单价列
|
||||
worksheet.getCell(7, 5).value = taxPriceHeaders[0];
|
||||
worksheet.getCell(8, 5).value = taxPriceHeaders[1];
|
||||
|
||||
// 不含税单价列
|
||||
worksheet.getCell(7, 6).value = noTaxPriceHeaders[0];
|
||||
worksheet.getCell(8, 6).value = noTaxPriceHeaders[1];
|
||||
|
||||
// 含税总额列
|
||||
worksheet.getCell(7, 7).value = taxTotalHeaders[0];
|
||||
worksheet.getCell(8, 7).value = taxTotalHeaders[1];
|
||||
|
||||
// 设置样式
|
||||
for (let row = 7; row <= 8; row++) {
|
||||
for (let col = 4; col <= 7; col++) {
|
||||
const cell = worksheet.getCell(row, col);
|
||||
cell.font = { bold: true };
|
||||
cell.fill = { type: 'pattern', pattern: 'solid' };
|
||||
cell.border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
cell.alignment = { horizontal: 'center', vertical: 'middle' };
|
||||
// 合计行单元格(按列顺序填充)
|
||||
let totalCells = '';
|
||||
let cellIdx = 0;
|
||||
activeCols.forEach((col) => {
|
||||
if (cellIdx < 2) {
|
||||
// 前 2 个活动列(spec、material)与序号一起被"合计" colspan=3 覆盖
|
||||
if (cellIdx === 0) {
|
||||
totalCells += `<td colspan="3" style="border:1px solid #000;padding:4px 4px;font-weight:bold;text-align:center;">合 计</td>`;
|
||||
}
|
||||
} else {
|
||||
let val = '';
|
||||
if (col.key === 'quantity') val = totalQty.toFixed(2);
|
||||
else if (col.key === 'taxTotal') val = totalTax.toFixed(2);
|
||||
else if (col.key === 'noTaxTotal') val = totalNoTax.toFixed(2);
|
||||
else if (col.key === 'taxAmount') val = totalTaxAmt.toFixed(2);
|
||||
totalCells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${val}</td>`;
|
||||
}
|
||||
}
|
||||
cellIdx++;
|
||||
});
|
||||
|
||||
const html = `
|
||||
<table style="width:100%;border-collapse:collapse;font-size:11px;margin-bottom:6px;">
|
||||
<tr>
|
||||
<td colspan="4" style="border:1px solid #000;padding:3px 6px;font-weight:bold;text-align:left;">产品名称:${productData.productName || ''}</td>
|
||||
<td colspan="${colCount - 4 > 0 ? colCount - 4 : 1}" style="border:1px solid #000;padding:3px 6px;font-weight:bold;text-align:left;">生产厂家:嘉祥科伦普重工有限公司</td>
|
||||
</tr>
|
||||
<tr style="background-color:#f5f5f5;">${headerCells}</tr>
|
||||
${bodyRows}
|
||||
<tr>${totalCells}</tr>
|
||||
<tr>
|
||||
<td colspan="${colCount}" style="border:1px solid #000;padding:4px 6px;font-weight:bold;text-align:left;">合计人民币(大写):${totalAmountInWords}</td>
|
||||
</tr>
|
||||
</table>`;
|
||||
return html;
|
||||
},
|
||||
/** 生成预览HTML并更新iframe */
|
||||
generatePreviewHtml() {
|
||||
const row = this.exportRow;
|
||||
if (!row) return;
|
||||
|
||||
// 产品表格数据, 来源于一个json字符串(productContent,json结构参考ProductContent.vue)
|
||||
// 解析产品内容
|
||||
let productData = parseProductContent(row.productContent);
|
||||
if (!productData.productName && row.productName) {
|
||||
productData.productName = row.productName;
|
||||
}
|
||||
const products = productData.products && productData.products.length > 0 ? productData.products : [];
|
||||
|
||||
// if (orderItems) {
|
||||
// try {
|
||||
// // 改为从orderItems中获取产品内容
|
||||
// const productName = orderItems[0].productType || '冷硬钢卷';
|
||||
// const remark = row.remark || '';
|
||||
// const products = orderItems.map(item => ({
|
||||
// spec: item.finishedProductSpec || '',
|
||||
// material: item.material || '',
|
||||
// quantity: parseFloat(item.weight || 0),
|
||||
// taxPrice: parseFloat(item.contractPrice || 0),
|
||||
// noTaxPrice: parseFloat(item.itemAmount || 0),
|
||||
// taxTotal: parseFloat(item.contractPrice) * parseFloat(item.weight || 0),
|
||||
// remark: item.remark || ''
|
||||
// }));
|
||||
const productTableHtml = this.buildProductTableHtml(productData, products);
|
||||
|
||||
// const totalQuantity = products.reduce((acc, product) => acc + parseFloat(product.quantity || 0), 0);
|
||||
// const totalTaxTotal = products.reduce((acc, product) => acc + parseFloat(product.taxTotal || 0), 0);
|
||||
// const totalAmountInWords = this.convertToChinese(totalTaxTotal);
|
||||
|
||||
// productData = {
|
||||
// products,
|
||||
// productName,
|
||||
// remark,
|
||||
// totalQuantity,
|
||||
// totalTaxTotal,
|
||||
// totalAmountInWords
|
||||
// };
|
||||
// } catch (error) {
|
||||
// console.error('解析产品内容失败:', error);
|
||||
// }
|
||||
// }
|
||||
|
||||
// 更新产品名称
|
||||
worksheet.getCell('A6').value = `产品名称:${productData.productName}`;
|
||||
|
||||
// 产品表格数据
|
||||
let currentRow = 9;
|
||||
let totalQuantity = 0;
|
||||
let totalTaxTotal = 0;
|
||||
|
||||
if (productData.products && productData.products.length > 0) {
|
||||
productData.products.forEach((product, index) => {
|
||||
const rowNum = currentRow + index;
|
||||
worksheet.getCell(`A${rowNum}`).value = index + 1;
|
||||
worksheet.getCell(`B${rowNum}`).value = product.spec || '';
|
||||
worksheet.getCell(`C${rowNum}`).value = product.material || '';
|
||||
worksheet.getCell(`D${rowNum}`).value = product.quantity || 0;
|
||||
worksheet.getCell(`E${rowNum}`).value = product.taxPrice || 0;
|
||||
worksheet.getCell(`F${rowNum}`).value = product.noTaxPrice || 0;
|
||||
worksheet.getCell(`G${rowNum}`).value = product.taxTotal || 0;
|
||||
worksheet.getCell(`H${rowNum}`).value = product.remark || '';
|
||||
|
||||
// 设置数据行边框
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
const cell = worksheet.getCell(rowNum, i);
|
||||
cell.border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
cell.alignment = { horizontal: 'center', vertical: 'middle' };
|
||||
}
|
||||
|
||||
// 累加合计
|
||||
totalQuantity += parseFloat(product.quantity || 0);
|
||||
totalTaxTotal += parseFloat(product.taxTotal || 0);
|
||||
});
|
||||
|
||||
currentRow += productData.products.length;
|
||||
} else {
|
||||
// 至少一行空数据
|
||||
worksheet.getCell(`A${currentRow}`).value = 1;
|
||||
worksheet.getCell(`B${currentRow}`).value = '';
|
||||
worksheet.getCell(`C${currentRow}`).value = 'SPCC';
|
||||
worksheet.getCell(`D${currentRow}`).value = '';
|
||||
worksheet.getCell(`E${currentRow}`).value = '';
|
||||
worksheet.getCell(`F${currentRow}`).value = 0.00;
|
||||
worksheet.getCell(`G${currentRow}`).value = 0;
|
||||
worksheet.getCell(`H${currentRow}`).value = '';
|
||||
|
||||
// 设置数据行边框
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
const cell = worksheet.getCell(currentRow, i);
|
||||
cell.border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
cell.alignment = { horizontal: 'center', vertical: 'middle' };
|
||||
}
|
||||
currentRow++;
|
||||
}
|
||||
|
||||
// 合计行
|
||||
worksheet.getCell(`A${currentRow}`).value = '合 计';
|
||||
worksheet.getCell(`A${currentRow}`).font = { bold: true };
|
||||
worksheet.getCell(`A${currentRow}`).border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
worksheet.getCell(`A${currentRow}`).alignment = { horizontal: 'center', vertical: 'middle' };
|
||||
|
||||
worksheet.getCell(`D${currentRow}`).value = productData.totalQuantity || totalQuantity;
|
||||
worksheet.getCell(`D${currentRow}`).border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
worksheet.getCell(`D${currentRow}`).alignment = { horizontal: 'center', vertical: 'middle' };
|
||||
|
||||
worksheet.getCell(`G${currentRow}`).value = productData.totalTaxTotal || totalTaxTotal;
|
||||
worksheet.getCell(`G${currentRow}`).border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
worksheet.getCell(`G${currentRow}`).alignment = { horizontal: 'center', vertical: 'middle' };
|
||||
|
||||
worksheet.getCell(`H${currentRow}`).value = '';
|
||||
worksheet.getCell(`H${currentRow}`).border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
worksheet.getCell(`H${currentRow}`).alignment = { horizontal: 'center', vertical: 'middle' };
|
||||
|
||||
currentRow++;
|
||||
|
||||
// 大写金额
|
||||
worksheet.mergeCells(`A${currentRow}:B${currentRow}`);
|
||||
worksheet.getCell(`A${currentRow}`).value = `合计人民币(大写)`;
|
||||
worksheet.getCell(`A${currentRow}`).font = { bold: true };
|
||||
worksheet.getCell(`A${currentRow}`).border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
worksheet.getCell(`A${currentRow}`).alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
|
||||
worksheet.mergeCells(`C${currentRow}:H${currentRow}`);
|
||||
worksheet.getCell(`C${currentRow}`).value = `${productData.totalAmountInWords || row.totalAmountUpper || '零元整'}`;
|
||||
worksheet.getCell(`C${currentRow}`).font = { bold: true };
|
||||
worksheet.getCell(`C${currentRow}`).border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
worksheet.getCell(`C${currentRow}`).alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
|
||||
currentRow++;
|
||||
|
||||
// 备注行
|
||||
worksheet.mergeCells(`B${currentRow}:H${currentRow}`);
|
||||
worksheet.getCell(`A${currentRow}`).value = '备注:';
|
||||
worksheet.getCell(`A${currentRow}`).alignment = { horizontal: 'left', vertical: 'top' };
|
||||
worksheet.getCell(`A${currentRow}`).border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
|
||||
worksheet.getCell(`B${currentRow}`).alignment = { horizontal: 'left', vertical: 'top' };
|
||||
worksheet.getCell(`B${currentRow}`).value = productData.remark || '';
|
||||
worksheet.getCell(`B${currentRow}`).border = {
|
||||
top: { style: 'thin' },
|
||||
left: { style: 'thin' },
|
||||
bottom: { style: 'thin' },
|
||||
right: { style: 'thin' }
|
||||
};
|
||||
worksheet.getCell(`B${currentRow}`).alignment = { horizontal: 'left', vertical: 'top', wrapText: true };
|
||||
|
||||
currentRow++;
|
||||
|
||||
// 空白行(保持至少5行空白)
|
||||
// const emptyRows = Math.max(5, 15 - currentRow + 1);
|
||||
// for (let i = 0; i < emptyRows; i++) {
|
||||
// const rowNum = currentRow + i;
|
||||
// for (let j = 1; j <= 8; j++) {
|
||||
// const cell = worksheet.getCell(rowNum, j);
|
||||
// cell.border = {
|
||||
// top: { style: 'thin' },
|
||||
// left: { style: 'thin' },
|
||||
// bottom: { style: 'thin' },
|
||||
// right: { style: 'thin' }
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
|
||||
// currentRow += emptyRows;
|
||||
|
||||
|
||||
|
||||
// 产品表格之后是其他合同内容(contractContent字段,存储的是HTML格式的富文本)
|
||||
// 解析合同内容
|
||||
let contractContentHtml = '';
|
||||
if (row.contractContent) {
|
||||
// 优化HTML处理,保留p标签作为换行符,将多个p标签单元格合并
|
||||
let htmlContent = row.contractContent;
|
||||
|
||||
// 提取所有p标签内容
|
||||
const pTagRegex = /<p[^>]*>([\s\S]*?)<\/p>/g;
|
||||
let match;
|
||||
const pContents = [];
|
||||
|
||||
while ((match = pTagRegex.exec(htmlContent)) !== null) {
|
||||
let content = match[1];
|
||||
|
||||
// 移除其他HTML标签
|
||||
content = content.replace(/<[^>]*>/g, '');
|
||||
|
||||
// 处理HTML实体
|
||||
content = content.replace(/ /g, ' ');
|
||||
content = content.replace(/</g, '<');
|
||||
content = content.replace(/>/g, '>');
|
||||
content = content.replace(/&/g, '&');
|
||||
content = content.replace(/"/g, '"');
|
||||
content = content.replace(/'/g, "'");
|
||||
|
||||
// 清理空格和换行
|
||||
content = content.trim();
|
||||
|
||||
// 如果不是以大写汉字数字开头,添加一个中文字符的缩进
|
||||
let content = match[1].replace(/<[^>]*>/g, '').replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'").trim();
|
||||
if (content) {
|
||||
// 检查是否以大写汉字数字开头(一、二、三、四、五、六、七、八、九、十)
|
||||
const chineseNumberRegex = /^[一二三四五六七八九十]+、/;
|
||||
if (!chineseNumberRegex.test(content)) {
|
||||
content = ' ' + content; // 使用全角空格作为中文字符缩进
|
||||
}
|
||||
pContents.push(content);
|
||||
pContents.push(chineseNumberRegex.test(content) ? content : ' ' + content);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有提取到p标签内容,尝试提取所有文本内容
|
||||
if (pContents.length === 0) {
|
||||
let textContent = htmlContent.replace(/<[^>]*>/g, '');
|
||||
textContent = textContent.replace(/ /g, ' ').trim();
|
||||
const textContent = htmlContent.replace(/<[^>]*>/g, '').replace(/ /g, ' ').trim();
|
||||
if (textContent) pContents.push(textContent);
|
||||
}
|
||||
if (pContents.length > 0) {
|
||||
contractContentHtml = '<div style="margin-top:8px;font-size:12px;line-height:1.8;">' + pContents.join('<br/>') + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是以大写汉字数字开头,添加一个中文字符的缩进
|
||||
if (textContent) {
|
||||
// 检查是否以大写汉字数字开头(一、二、三、四、五、六、七、八、九、十)
|
||||
const chineseNumberRegex = /^[一二三四五六七八九十]+、/;
|
||||
if (!chineseNumberRegex.test(textContent)) {
|
||||
textContent = ' ' + textContent; // 使用全角空格作为中文字符缩进
|
||||
const totalTax = products.reduce((a, p) => a + (parseFloat(p.taxTotal) || 0), 0);
|
||||
const amountWords = productData.totalAmountInWords || convertToChinese(totalTax) || '零元整';
|
||||
|
||||
const fullHtml = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>合同预览</title>
|
||||
<style>
|
||||
* { 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: 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: collapse; }
|
||||
th, td { border: 1px solid #000; padding: 3px 4px; text-align: center; font-size: 11px; }
|
||||
th { background-color: #f5f5f5; font-weight: bold; }
|
||||
.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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
${productTableHtml}
|
||||
${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>`;
|
||||
|
||||
this.$nextTick(() => {
|
||||
const iframe = this.$refs.previewFrame;
|
||||
if (iframe) {
|
||||
iframe.srcdoc = fullHtml;
|
||||
}
|
||||
});
|
||||
},
|
||||
/** 确认导出PDF */
|
||||
async confirmExport() {
|
||||
const row = this.exportRow;
|
||||
if (!row) return;
|
||||
|
||||
this.exportLoading = true;
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在生成PDF,请稍候...',
|
||||
background: 'rgba(0, 0, 0, 0.7)'
|
||||
});
|
||||
|
||||
try {
|
||||
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 productTableHtml = this.buildProductTableHtml(productData, products);
|
||||
|
||||
// 解析合同内容
|
||||
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);
|
||||
}
|
||||
pContents.push(textContent);
|
||||
}
|
||||
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>';
|
||||
}
|
||||
}
|
||||
|
||||
// 直接合并单元格并设置内容,避免合并已合并的单元格
|
||||
if (pContents.length > 0) {
|
||||
// 计算需要的行数
|
||||
const contentLines = pContents.reduce((total, content) => {
|
||||
return total + (content.match(/\n/g) || []).length + 1;
|
||||
}, 0);
|
||||
const totalTax = products.reduce((a, p) => a + (parseFloat(p.taxTotal) || 0), 0);
|
||||
const amountWords = productData.totalAmountInWords || convertToChinese(totalTax) || '零元整';
|
||||
|
||||
// 合并单元格
|
||||
const endRow = currentRow + Math.ceil(contentLines / 2); // 估算需要的行数
|
||||
worksheet.mergeCells(`A${currentRow}:H${endRow}`);
|
||||
// 构建完整的合同HTML(794px 宽,适合A4)
|
||||
const contractHtml = `
|
||||
<div id="contract-pdf-content" style="width:794px;padding:30px 40px;font-family:'SimSun','宋体',serif;color:#000;background:#fff;box-sizing:border-box;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 style="display:flex;justify-content:space-between;margin-bottom:8px;font-size:13px;line-height:2;">
|
||||
<div style="width:55%;">
|
||||
<div>供方(甲方):${row.supplier || ''}</div>
|
||||
<div>需方(乙方):${row.customer || ''}</div>
|
||||
</div>
|
||||
<div style="width:40%;text-align:left;">
|
||||
<div>签订时间:${row.signTime ? this.parseTime(row.signTime, '{y}年{m}月{d}日') : ''}</div>
|
||||
<div>签订地点:${row.signLocation || ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:13px;font-weight:bold;margin-bottom:5px;">一、产品内容</div>
|
||||
${productTableHtml}
|
||||
${contractContentHtml}
|
||||
<div style="margin-top:40px;font-size:12px;line-height:2.2;">
|
||||
<div style="display:flex;justify-content:space-between;">
|
||||
<div style="width:48%;">
|
||||
<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 style="width:48%;">
|
||||
<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>
|
||||
`;
|
||||
|
||||
// 设置合并后单元格的内容和样式
|
||||
const mergedContent = pContents.join('\n');
|
||||
worksheet.getCell(`A${currentRow}`).value = mergedContent;
|
||||
worksheet.getCell(`A${currentRow}`).alignment = { horizontal: 'left', vertical: 'top', wrapText: true };
|
||||
const container = document.createElement('div');
|
||||
container.style.position = 'fixed';
|
||||
container.style.left = '-9999px';
|
||||
container.style.top = '0';
|
||||
container.innerHTML = contractHtml;
|
||||
document.body.appendChild(container);
|
||||
|
||||
// 调整合并后单元格的行高
|
||||
const lineCount = (mergedContent.match(/\n/g) || []).length + 1;
|
||||
const height = Math.max(60, lineCount * 15);
|
||||
worksheet.getRow(currentRow).height = height;
|
||||
const element = document.getElementById('contract-pdf-content');
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// 调整currentRow
|
||||
currentRow = endRow + 1;
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
logging: false,
|
||||
backgroundColor: '#ffffff',
|
||||
imageTimeout: 5000
|
||||
});
|
||||
|
||||
document.body.removeChild(container);
|
||||
|
||||
const pdfWidth = 210;
|
||||
const pdfHeight = 297;
|
||||
const imgWidth = pdfWidth;
|
||||
const imgHeight = (canvas.height * pdfWidth) / canvas.width;
|
||||
const pdf = new jsPDF('p', 'mm', 'a4');
|
||||
const imgData = canvas.toDataURL('image/jpeg', 0.95);
|
||||
let heightLeft = imgHeight;
|
||||
let position = 0;
|
||||
|
||||
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
|
||||
heightLeft -= pdfHeight;
|
||||
|
||||
while (heightLeft > 0) {
|
||||
position = heightLeft - imgHeight;
|
||||
pdf.addPage();
|
||||
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
|
||||
heightLeft -= pdfHeight;
|
||||
}
|
||||
|
||||
pdf.save(`合同_${row.contractCode || row.contractName || '未命名'}.pdf`);
|
||||
this.$message.success('PDF导出成功');
|
||||
this.exportDialogVisible = false;
|
||||
} catch (error) {
|
||||
console.error('PDF导出失败:', error);
|
||||
this.$message.error('PDF导出失败,请重试');
|
||||
} finally {
|
||||
loading.close();
|
||||
this.exportLoading = false;
|
||||
}
|
||||
|
||||
// 设置合同尾部信息,每一行的前四列与后四列分别合并
|
||||
// 内容参考
|
||||
|
||||
// 设置合同尾部信息,每一行的前四列与后四列分别合并
|
||||
// 内容参考ContractPreview.vue
|
||||
if (currentRow > 0) {
|
||||
// 空行
|
||||
currentRow++;
|
||||
|
||||
// 供方(甲方)信息
|
||||
worksheet.mergeCells(`A${currentRow}:D${currentRow}`);
|
||||
worksheet.getCell(`A${currentRow}`).value = `供方(甲方):${row.supplier || '嘉祥科伦普重工有限公司'}`;
|
||||
worksheet.getCell(`A${currentRow}`).alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
|
||||
worksheet.mergeCells(`E${currentRow}:H${currentRow}`);
|
||||
worksheet.getCell(`E${currentRow}`).value = `需方(乙方):${row.customer || ''}`;
|
||||
worksheet.getCell(`E${currentRow}`).alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
currentRow++;
|
||||
|
||||
// 地址信息
|
||||
worksheet.mergeCells(`A${currentRow}:D${currentRow}`);
|
||||
worksheet.getCell(`A${currentRow}`).value = `地址:${row.supplierAddress || ''}`;
|
||||
worksheet.getCell(`A${currentRow}`).alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
|
||||
worksheet.mergeCells(`E${currentRow}:H${currentRow}`);
|
||||
worksheet.getCell(`E${currentRow}`).value = `地址:${row.customerAddress || ''}`;
|
||||
worksheet.getCell(`E${currentRow}`).alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
currentRow++;
|
||||
|
||||
// 电话信息
|
||||
worksheet.mergeCells(`A${currentRow}:D${currentRow}`);
|
||||
worksheet.getCell(`A${currentRow}`).value = `电话:${row.supplierPhone || ''}`;
|
||||
worksheet.getCell(`A${currentRow}`).alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
|
||||
worksheet.mergeCells(`E${currentRow}:H${currentRow}`);
|
||||
worksheet.getCell(`E${currentRow}`).value = `电话:${row.customerPhone || ''}`;
|
||||
worksheet.getCell(`E${currentRow}`).alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
currentRow++;
|
||||
|
||||
// 开户行信息
|
||||
worksheet.mergeCells(`A${currentRow}:D${currentRow}`);
|
||||
worksheet.getCell(`A${currentRow}`).value = `开户行:${row.supplierBank || ''}`;
|
||||
worksheet.getCell(`A${currentRow}`).alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
|
||||
worksheet.mergeCells(`E${currentRow}:H${currentRow}`);
|
||||
worksheet.getCell(`E${currentRow}`).value = `开户行:${row.customerBank || ''}`;
|
||||
worksheet.getCell(`E${currentRow}`).alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
currentRow++;
|
||||
|
||||
// 账号信息
|
||||
worksheet.mergeCells(`A${currentRow}:D${currentRow}`);
|
||||
worksheet.getCell(`A${currentRow}`).value = `账号:${row.supplierAccount || ''}`;
|
||||
worksheet.getCell(`A${currentRow}`).alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
|
||||
worksheet.mergeCells(`E${currentRow}:H${currentRow}`);
|
||||
worksheet.getCell(`E${currentRow}`).value = `账号:${row.customerAccount || ''}`;
|
||||
worksheet.getCell(`E${currentRow}`).alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
currentRow++;
|
||||
|
||||
// 税号信息
|
||||
worksheet.mergeCells(`A${currentRow}:D${currentRow}`);
|
||||
worksheet.getCell(`A${currentRow}`).value = `税号:${row.supplierTaxNo || ''}`;
|
||||
worksheet.getCell(`A${currentRow}`).alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
|
||||
worksheet.mergeCells(`E${currentRow}:H${currentRow}`);
|
||||
worksheet.getCell(`E${currentRow}`).value = `税号:${row.customerTaxNo || ''}`;
|
||||
worksheet.getCell(`E${currentRow}`).alignment = { horizontal: 'left', vertical: 'middle' };
|
||||
currentRow++;
|
||||
}
|
||||
|
||||
// 4. 设置行高
|
||||
worksheet.getRow(1).height = 40;
|
||||
worksheet.getRow(2).height = 30;
|
||||
worksheet.getRow(3).height = 20;
|
||||
worksheet.getRow(4).height = 20;
|
||||
worksheet.getRow(5).height = 20;
|
||||
worksheet.getRow(6).height = 20;
|
||||
worksheet.getRow(7).height = 25;
|
||||
|
||||
// 自动调整数据行高
|
||||
for (let i = 8; i <= currentRow; i++) {
|
||||
if (!worksheet.getRow(i).height) {
|
||||
worksheet.getRow(i).height = 20;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 导出文件
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
saveAs(blob, `合同_${row.contractCode || row.contractName || '未命名'}.xlsx`);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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(/ /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: '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;">合 计</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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -828,6 +828,10 @@ export default {
|
||||
this.selectedSplitItem = null
|
||||
this.resetSplitForm()
|
||||
this.splitForm.enterCoilNo = this.coilInfo.enterCoilNo || ''
|
||||
// 如果是镀铬工序,设置默认镀铬卷号:取父卷入场卷号(多个逗号分隔时取第一个)
|
||||
if (this.isGrindAction && this.coilInfo.enterCoilNo) {
|
||||
this.splitForm.chromePlateCoilNo = this.coilInfo.enterCoilNo.split(',')[0].trim()
|
||||
}
|
||||
// 查询是否有缓存
|
||||
try {
|
||||
const res = await getCoilCacheByCoilId(this.coilId)
|
||||
|
||||
Reference in New Issue
Block a user