Files
xgy-oa/klp-ui/src/views/wms/delivery/components/wayBill2.vue
砂糖 6f6acf0c3c feat(发货单): 添加简单打印功能
新增简单打印功能按钮,支持选择不同打印模板。引入WayBill2组件作为简单打印模板,通过printType控制显示不同模板。
2026-03-11 13:10:11 +08:00

1390 lines
46 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>
<div class="waybill-container" ref="waybillRef">
<div class="waybill-content">
<!-- 头部信息 -->
<!-- 标题信息 -->
<div class="title">科伦普发货单</div>
<div class="waybill-header">
<div class="header-left">
<span class="label">收货单位</span>
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.consigneeUnit }}</div>
</div>
<div class="header-center">
<div class="editable-input date-input transparent-input" contenteditable>{{ localWaybill.deliveryYear }}
</div>
<span class="label date-label"></span>
<div class="editable-input date-input transparent-input" contenteditable>{{ localWaybill.deliveryMonth }}
</div>
<span class="label date-label"></span>
<div class="editable-input date-input transparent-input" contenteditable>{{ localWaybill.deliveryDay }}
</div>
<span class="label date-label"></span>
</div>
<div class="header-right">
<span class="label">发货单位</span>
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.senderUnit }}</div>
</div>
</div>
<div class="waybill-header">
<div class="header-left">
<span class="label">负责人</span>
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.principal }}</div>
</div>
<div class="header-right">
<span class="label">电话</span>
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.principalPhone }}</div>
</div>
<div class="header-right">
<span class="label">合同号</span>
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.contractCode }}</div>
</div>
<div class="header-center">
<span class="label">车牌</span>
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.licensePlate }}</div>
</div>
</div>
<!-- 表格 -->
<table class="waybill-table">
<thead>
<tr>
<th>品名</th>
<th>切边</th>
<th>包装</th>
<th>仓库位置</th>
<th>结算</th>
<th>原料厂家</th>
<th>卷号</th>
<th>规格</th>
<th>材质</th>
<!-- <th>数量</th> -->
<th>重量(t)</th>
<!-- <th>单价</th> -->
<th>备注</th>
</tr>
</thead>
<tbody>
<!-- 无明细提示 -->
<tr v-if="localWaybillDetails.length === 0">
<td colspan="12" class="no-data">
<div class="no-data-content">
<el-empty description="暂无发货单明细" />
</div>
</td>
</tr>
<!-- 明细数据 -->
<tr v-for="(item, index) in displayWaybillDetails" :key="index">
<td>
<div class="table-input transparent-input" contenteditable>{{ item.productName }}</div>
</td>
<td>
<div class="table-input transparent-input" contenteditable>{{ item.edgeType }}</div>
</td>
<td>
<div class="table-input transparent-input" contenteditable>{{ item.packageType }}</div>
</td>
<td>
<div class="table-input transparent-input" contenteditable>{{ item.actualWarehouseName }}</div>
</td>
<td>
<div class="table-input transparent-input" contenteditable>{{ item.settlementType }}</div>
</td>
<td>
<div class="table-input transparent-input" contenteditable>{{ item.rawMaterialFactory }}</div>
</td>
<td>
<div class="table-input transparent-input" contenteditable>{{ item.coilNumber }}</div>
</td>
<td>
<div class="table-input transparent-input" contenteditable>{{ item.specification }}</div>
</td>
<td>
<div class="table-input transparent-input" contenteditable>{{ item.material }}</div>
</td>
<!-- <td><input type="number" class="table-input transparent-input" v-model.number="item.quantity"
placeholder="0" /></td> -->
<td><input type="number" class="table-input transparent-input" v-model.number="item.weight"
placeholder="0.00" /></td>
<!-- <td>
<div class="table-input transparent-input" contenteditable>{{ item.unitPrice }}</div>
</td> -->
<td>
<div class="table-input transparent-input" contenteditable>{{ item.remark }}</div>
</td>
</tr>
<!-- 加粗最后一行的线 -->
<tr style="height: 0;">
<td style="height: 0;"></td>
<td style="height: 0;"></td>
<td style="height: 0;"></td>
<td style="height: 0;"></td>
<td style="height: 0;"></td>
<td style="height: 0;"></td>
<td style="height: 0;"></td>
<td style="height: 0;"></td>
<td style="height: 0;"></td>
<!-- <td><input type="number" class="table-input transparent-input" v-model.number="item.quantity"
placeholder="0" /></td> -->
<!-- <td style="height: 0;"></td> -->
<td style="height: 0;"></td>
<td style="height: 0;"></td>
</tr>
</tbody>
</table>
<!-- 备注说明 -->
<div class="waybill-remarks">
<p>
1品名冷硬钢卷酸连轧冷轧钢卷脱脂退火火拉矫镀锌卷板镀锌管料镀锌分剪料2切边净边/毛边3包装裸包周三径四简包1周三径四内外护角简包2周三径四+防锈纸普包周三径四+内外护角+防锈纸+端护板精包1周三径四+内外护角+防锈纸+薄膜+端护板+内外护板精包2周三径四+内外护角+防锈纸+薄膜+端护板+内外护板+木托
</p>
</div>
<div class="waybill-pickup-location">
<!-- <div class="pickup-location-item inline"> -->
<span style="font-size: 18px; font-weight: bold;">取货地点</span>
<input type="text" class="editable-input full-input transparent-input"
v-model="localWaybill.pickupLocation" />
<!-- </div> -->
</div>
<!-- 签名栏 -->
<div class="waybill-footer">
<!-- <div class="footer-item inline">
<span class="label">销售</span>
<div class="editable-input signature-input transparent-input" contenteditable>{{ localWaybill.salesman }}</div>
</div> -->
<div class="footer-item inline">
<span class="label">发货</span>
<div class="editable-input signature-input transparent-input" contenteditable>{{ localWaybill.deliveryman }}
</div>
</div>
<div class="footer-item inline">
<span class="label">司机</span>
<div class="editable-input signature-input transparent-input" contenteditable>{{ localWaybill.driver }}
</div>
</div>
<div class="footer-item inline">
<span class="label">磅房</span>
<div class="editable-input signature-input transparent-input" contenteditable>{{ localWaybill.weightRoom }}
</div>
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="waybill-actions">
<div class="waybill-pagebar">
<el-button size="mini" @click="changePage(currentPage - 1)" :disabled="currentPage <= 1">上一页</el-button>
<span class="page-info"> {{ currentPage }} / {{ totalPages }} </span>
<el-button size="mini" @click="changePage(currentPage + 1)"
:disabled="currentPage >= totalPages">下一页</el-button>
</div>
<el-button type="primary" @click="saveAsImage">保存为图片</el-button>
<el-button type="success" @click="printWaybill">打印</el-button>
<el-button type="warning" @click="exportExcel">导出Excel</el-button>
</div>
</div>
</template>
<script>
import domtoimage from 'dom-to-image';
import { PDFDocument } from 'pdf-lib';
import html2canvas from 'html2canvas';
import * as XLSX from 'xlsx';
import ExcelJS from 'exceljs';
export default {
props: {
// 组件接收完整的发货单内容, 渲染发货单,这个面板内包括一个保存为图片(已经安装了dom-to-image)和一个打印按钮(已经安装了print-js)
waybill: {
type: Object,
default: () => { }
},
waybillDetails: {
type: Array,
default: () => []
}
},
data() {
return {
currentPage: 1,
perPage: 7,
totalPages: 1,
// 本地可编辑的发货单数据
localWaybill: {
consigneeUnit: '',
senderUnit: '',
deliveryYear: '',
deliveryMonth: '',
deliveryDay: '',
principal: '',
principalPhone: '',
licensePlate: '',
pickupLocation: ''
},
// 本地可编辑的发货单明细
localWaybillDetails: [],
// 预览/打印用明细每页固定7行不足补空行
displayWaybillDetails: []
};
},
watch: {
// 监听props变化更新本地数据
waybill: {
handler(newVal) {
if (newVal) {
this.localWaybill = {
consigneeUnit: newVal.consigneeUnit || '',
senderUnit: newVal.senderUnit || '',
deliveryYear: this.getYearFromDate(newVal.deliveryTime) || '',
deliveryMonth: this.getMonthFromDate(newVal.deliveryTime) || '',
deliveryDay: this.getDayFromDate(newVal.deliveryTime) || '',
principal: newVal.principal || '',
principalPhone: newVal.principalPhone || '',
licensePlate: newVal.licensePlate || '',
pickupLocation: newVal.pickupLocation || '',
contractCode: newVal.contractCode || ''
};
}
},
immediate: true,
deep: true
},
waybillDetails: {
handler(newVal) {
if (newVal && Array.isArray(newVal)) {
this.localWaybillDetails = [...newVal];
} else {
this.localWaybillDetails = [];
}
this.refreshPagination();
},
immediate: true,
deep: true
}
},
methods: {
buildPage(details, page) {
const perPage = this.perPage;
const src = Array.isArray(details) ? details : [];
const start = (page - 1) * perPage;
const pageRows = src.slice(start, start + perPage).map((x) => ({ ...x }));
while (pageRows.length < perPage) {
pageRows.push({
productName: '',
edgeType: '',
packageType: '',
actualWarehouseName: '',
settlementType: '',
rawMaterialFactory: '',
coilNumber: '',
specification: '',
material: '',
quantity: '',
weight: '',
unitPrice: ''
});
}
return pageRows;
},
readWaybill() {
const file = this.$refs.file.files[0];
if (!file) {
return;
}
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = (e) => {
const data = e.target.result;
const wb = XLSX.read(data, { type: 'array' });
const ws = wb.Sheets[wb.SheetNames[0]];
const json = XLSX.utils.sheet_to_eth(ws);
console.log(json);
// this.localWaybillDetails = json;
// this.refreshPagination();
};
reader.onerror = (e) => {
console.error('读取文件失败:', e);
};
},
refreshPagination() {
const src = Array.isArray(this.localWaybillDetails) ? this.localWaybillDetails : [];
this.totalPages = Math.max(1, Math.ceil(src.length / this.perPage));
if (this.currentPage > this.totalPages) this.currentPage = this.totalPages;
if (this.currentPage < 1) this.currentPage = 1;
this.displayWaybillDetails = this.buildPage(src, this.currentPage);
},
changePage(next) {
this.currentPage = next;
this.refreshPagination();
},
// 从日期字符串中提取年份
getYearFromDate(dateStr) {
if (dateStr) {
const date = new Date(dateStr);
return date.getFullYear().toString();
}
return '';
},
// 从日期字符串中提取月份
getMonthFromDate(dateStr) {
if (dateStr) {
const date = new Date(dateStr);
return (date.getMonth() + 1).toString();
}
return '';
},
// 从日期字符串中提取日
getDayFromDate(dateStr) {
if (dateStr) {
const date = new Date(dateStr);
return date.getDate().toString();
}
return '';
},
// 保存为图片
saveAsImage() {
const node = this.$refs.waybillRef;
// 确保容器在保存图片时能完整显示所有内容
const originalWidth = node.style.width;
const originalOverflow = node.style.overflow;
// 临时调整容器样式,确保所有内容可见
node.style.width = 'auto';
node.style.overflow = 'visible';
// 获取实际内容宽度
const contentWidth = node.scrollWidth;
const contentHeight = node.scrollHeight;
domtoimage.toPng(node, {
width: contentWidth,
height: contentHeight,
style: {
width: `${contentWidth}px`,
height: `${contentHeight}px`,
overflow: 'visible'
}
})
.then(dataUrl => {
const link = document.createElement('a');
link.download = `发货单_${this.waybill.waybillName || this.waybill.waybillNo || Date.now()}.png`;
link.href = dataUrl;
link.click();
})
.catch(error => {
console.error('保存图片失败:', error);
this.$message.error('保存图片失败');
})
.finally(() => {
// 恢复原始样式
node.style.width = originalWidth;
node.style.overflow = originalOverflow;
});
},
// 打印发货单
async printWaybill() {
const node = this.$refs.waybillRef;
const paperWidthMm = 241;
const paperHeightMm = 140;
const safeRightMarginMm = 45;
const safeLeftPaddingMm = 6;
const safeTopShiftMm = 15;
const safeContentWidthMm = paperWidthMm - safeRightMarginMm - safeLeftPaddingMm;
const mmToPx = (mm) => (mm * 96) / 25.4;
const mmToPt = 72 / 25.4;
const pageWidthPt = paperWidthMm * mmToPt;
const pageHeightPt = paperHeightMm * mmToPt;
const pageWidthPx = Math.round(mmToPx(paperWidthMm));
const pageHeightPx = Math.round(mmToPx(paperHeightMm));
const allDetails = Array.isArray(this.localWaybillDetails) ? this.localWaybillDetails : [];
const totalPages = Math.max(1, Math.ceil(allDetails.length / this.perPage));
const originalPage = this.currentPage;
const originalDisplay = this.displayWaybillDetails;
const pdfDoc = await PDFDocument.create();
// 强制让容器内容不被裁剪以便html2canvas完整捕获
const originalStyle = { width: node.style.width, overflow: node.style.overflow };
const contentEl = node.querySelector('.waybill-content');
const originalContentCssText = contentEl ? contentEl.style.cssText : '';
node.style.width = 'auto';
node.style.overflow = 'visible';
// 针对“导出PDF再打印”的场景为打印机右侧不可打印区预留约30mm
// 通过缩放内容区域不换行、不省略让最终PDF右侧自带留白
if (contentEl) {
contentEl.style.setProperty('--content-width-mm', String(safeContentWidthMm));
contentEl.style.setProperty('--offset-x-mm', String(safeLeftPaddingMm));
contentEl.style.setProperty('--offset-y-mm', String(safeTopShiftMm));
}
try {
for (let page = 1; page <= totalPages; page++) {
this.currentPage = page;
this.displayWaybillDetails = this.buildPage(allDetails, page);
await this.$nextTick();
// 固定按纸张尺寸截图:右侧留白来自于 contentEl 的缩放241 -> 211mm而不是扩大截图宽度
const canvas = await html2canvas(node, {
backgroundColor: '#ffffff',
scale: 3,
useCORS: true,
willReadFrequently: true,
width: pageWidthPx,
height: pageHeightPx,
windowWidth: pageWidthPx,
windowHeight: pageHeightPx,
scrollX: 0,
scrollY: 0
});
const png = canvas.toDataURL('image/png');
const imgPng = await pdfDoc.embedPng(png);
const pdfPage = pdfDoc.addPage([pageWidthPt, pageHeightPt]);
pdfPage.drawImage(imgPng, {
x: 0,
y: 0,
width: pageWidthPt,
height: pageHeightPt
});
}
} finally {
// 恢复截图前的容器样式
node.style.width = originalStyle.width;
node.style.overflow = originalStyle.overflow;
if (contentEl) contentEl.style.cssText = originalContentCssText;
this.currentPage = originalPage;
this.displayWaybillDetails = originalDisplay;
this.refreshPagination();
}
const pdfBytes = await pdfDoc.save();
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const win = window.open(url, '_blank');
if (!win) {
const a = document.createElement('a');
a.href = url;
a.download = `发货单_${this.waybill.waybillName || this.waybill.waybillNo || new Date().getTime()}.pdf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
},
async exportExcelByXLSX() {
try {
// ===== 1. 构建真实数据确保列数为11列0-10=====
const headerData = [
['科伦普发货单'], // 标题行r=0
[
`收货单位:${this.localWaybill.consigneeUnit || ''}`,
undefined, undefined, undefined,
`${this.localWaybill.deliveryYear || ''}${this.localWaybill.deliveryMonth || ''}${this.localWaybill.deliveryDay || ''}`,
undefined, undefined,
`发货单位:${this.localWaybill.senderUnit || ''}`
], // 收货/日期/发货单位行r=1
[
`负责人:${this.localWaybill.principal || ''}`,
undefined, undefined,
`电话:${this.localWaybill.principalPhone || ''}`,
undefined, undefined,
`合同号:${this.localWaybill.contractCode || ''}`,
undefined,
`车牌:${this.localWaybill.licensePlate || ''}`
], // 负责人/电话/合同号/车牌行r=2
["品名", '切边', '包装', '仓库位置', '结算', '原料厂家', '卷号', '规格', '材质', '重量(t)', '备注'], // 表格表头r=311列
];
// 表格明细行确保11列
const detailRows = this.displayWaybillDetails.map(item => [
item.productName || '',
item.edgeType || '',
item.packageType || '',
item.actualWarehouseName || '',
item.settlementType || '',
item.rawMaterialFactory || '',
item.coilNumber || '',
item.specification || '',
item.material || '',
item.weight || '',
item.remark || '' // 第11列备注无多余列
]);
// 底部备注/取货地点/签名栏(确保列数匹配)
const footerData = [
[
'1、品名冷硬钢卷酸连轧、冷轧钢卷脱脂退火火拉矫、镀锌卷板镀锌管料镀锌分剪料2、切边净边/毛边3、包装裸包周三径四简包1周三径四内外护角简包2周三径四+防锈纸;普包:周三径四+内外护角+防锈纸+端护板精包1周三径四+内外护角+防锈纸+薄膜+端护板+内外护板精包2周三径四+内外护角+防锈纸+薄膜+端护板+内外护板+木托。'
], // 备注行
[`取货地点:${this.localWaybill.pickupLocation || ''}`], // 取货地点行
[
`发货:${this.localWaybill.deliveryman || ''}`,
undefined, undefined, undefined,
`司机:${this.localWaybill.driver || ''}`,
undefined, undefined,
`磅房:${this.localWaybill.weightRoom || ''}`
] // 签名栏行
];
// 合并所有数据行
const data = [...headerData, ...detailRows, ...footerData];
const titleRowIdx = 0;
const headerRow1Idx = 1;
const headerRow2Idx = 2;
const tableHeaderIdx = 3;
const remarkRowIdx = 3 + detailRows.length;
const pickupRowIdx = remarkRowIdx + 1;
const footerRowIdx = pickupRowIdx + 1;
// ===== 2. 创建工作表并配置合并核心e.c改为10对应最后一列备注=====
const ws = XLSX.utils.aoa_to_sheet(data);
ws["!cellStyles"] = true;
ws["!merges"] = [
{ s: { c: 0, r: titleRowIdx }, e: { c: 10, r: titleRowIdx } }, // 标题合并0-10列无多余
{ s: { c: 0, r: headerRow1Idx }, e: { c: 3, r: headerRow1Idx } }, // 收货单位0-3
{ s: { c: 4, r: headerRow1Idx }, e: { c: 6, r: headerRow1Idx } }, // 日期4-7
{ s: { c: 8, r: headerRow1Idx }, e: { c: 10, r: headerRow1Idx } }, // 发货单位8-10无多余
{ s: { c: 0, r: headerRow2Idx }, e: { c: 2, r: headerRow2Idx } }, // 负责人0-2
{ s: { c: 3, r: headerRow2Idx }, e: { c: 5, r: headerRow2Idx } }, // 电话3-5
{ s: { c: 6, r: headerRow2Idx }, e: { c: 7, r: headerRow2Idx } }, // 合同号6-8
{ s: { c: 9, r: headerRow2Idx }, e: { c: 10, r: headerRow2Idx } }, // 车牌9-10无多余
{ s: { c: 0, r: remarkRowIdx }, e: { c: 10, r: remarkRowIdx } }, // 备注合并0-10列
{ s: { c: 0, r: pickupRowIdx }, e: { c: 10, r: pickupRowIdx } }, // 取货地点合并0-10列
{ s: { c: 0, r: footerRowIdx }, e: { c: 3, r: footerRowIdx } }, // 发货0-3
{ s: { c: 4, r: footerRowIdx }, e: { c: 6, r: footerRowIdx } }, // 司机4-7
{ s: { c: 8, r: footerRowIdx }, e: { c: 10, r: footerRowIdx } }, // 磅房8-10无多余
];
// ===== 3. 配置列宽仅11列对应0-10索引=====
ws['!cols'] = [
{ wpx: 70 }, // 品名0
{ wpx: 40 }, // 切边1
{ wpx: 50 }, // 包装2
{ wpx: 90 }, // 仓库位置3
{ wpx: 60 }, // 结算4
{ wpx: 70 }, // 原料厂家5
{ wpx: 150 }, // 卷号6
{ wpx: 90 }, // 规格7
{ wpx: 80 }, // 材质8
{ wpx: 70 }, // 重量(t)9
{ wpx: 130 }, // 备注10→ 最后一列,无多余
];
// ===== 4. 单元格样式配置(无修改)=====
const createCellStyle = (options = {}) => {
const defaultStyle = {
font: {
name: "SimSun",
sz: options.sz || 16,
bold: options.bold ?? true,
},
alignment: {
horizontal: options.horizontal || "center",
vertical: options.vertical || "center",
wrapText: options.wrapText || false,
},
border: options.border || {
top: { style: "thin", color: { rgb: "000000" } },
bottom: { style: "thin", color: { rgb: "000000" } },
left: { style: "thin", color: { rgb: "000000" } },
right: { style: "thin", color: { rgb: "000000" } },
},
};
return defaultStyle;
};
// 标题单元格
const titleCell = XLSX.utils.encode_cell({ r: titleRowIdx, c: 0 });
ws[titleCell].s = {
...createCellStyle({
sz: 24,
border: null,
}),
};
// 头部信息行
const headerCells = [
XLSX.utils.encode_cell({ r: headerRow1Idx, c: 0 }),
XLSX.utils.encode_cell({ r: headerRow1Idx, c: 4 }),
XLSX.utils.encode_cell({ r: headerRow1Idx, c: 8 }),
XLSX.utils.encode_cell({ r: headerRow2Idx, c: 0 }),
XLSX.utils.encode_cell({ r: headerRow2Idx, c: 3 }),
XLSX.utils.encode_cell({ r: headerRow2Idx, c: 6 }),
XLSX.utils.encode_cell({ r: headerRow2Idx, c: 9 }),
];
headerCells.forEach(cellAddr => {
if (ws[cellAddr]) {
ws[cellAddr].s = {
...createCellStyle({
sz: 18,
border: null,
}),
};
}
});
// 表格表头
for (let c = 0; c < 11; c++) { // 仅循环0-10列
const cellAddr = XLSX.utils.encode_cell({ r: tableHeaderIdx, c });
if (ws[cellAddr]) {
ws[cellAddr].s = createCellStyle({ sz: 16 });
}
}
// 表格明细行
for (let r = tableHeaderIdx + 1; r < remarkRowIdx; r++) {
for (let c = 0; c < 11; c++) { // 仅循环0-10列
const cellAddr = XLSX.utils.encode_cell({ r, c });
if (ws[cellAddr]) {
ws[cellAddr].s = createCellStyle({ sz: 16 });
}
}
}
// 备注行
const remarkCell = XLSX.utils.encode_cell({ r: remarkRowIdx, c: 0 });
if (ws[remarkCell]) {
ws[remarkCell].s = {
...createCellStyle({
sz: 18,
wrapText: true,
border: null,
}),
};
}
// 取货地点行
const pickupCell = XLSX.utils.encode_cell({ r: pickupRowIdx, c: 0 });
if (ws[pickupCell]) {
ws[pickupCell].s = {
...createCellStyle({
sz: 18,
border: null,
}),
};
}
// 签名栏行
const footerCells = [
XLSX.utils.encode_cell({ r: footerRowIdx, c: 0 }),
XLSX.utils.encode_cell({ r: footerRowIdx, c: 4 }),
XLSX.utils.encode_cell({ r: footerRowIdx, c: 8 }),
];
footerCells.forEach(cellAddr => {
if (ws[cellAddr]) {
ws[cellAddr].s = {
...createCellStyle({
sz: 18,
border: null,
}),
};
}
});
// ===== 5. 生成Excel并下载无修改=====
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "科伦普发货单");
const excelBuffer = XLSX.write(wb, {
bookType: "xlsx",
type: "array",
cellStyles: true,
sheetStubs: true
});
const blob = new Blob([excelBuffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8",
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `科伦普发货单_${this.waybill.waybillNo || Date.now()}.xlsx`;
document.body.appendChild(link);
link.click();
setTimeout(() => {
document.body.removeChild(link);
URL.revokeObjectURL(url);
}, 100);
} catch (error) {
console.error("Excel导出失败", error);
this.$message.error("导出失败,请重试");
}
},
async exportExcel() {
try {
// 1. 创建工作簿和工作表
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('科伦普发货单');
worksheet.properties.defaultRowHeight = 25; // 设置默认行高匹配Web端行高
// 2. 构建数据(复用原有业务数据)
const title = `科伦普发货单`; // 标题
const header1 = { // 收货单位+日期+发货单位
consignee: `收货单位:${this.localWaybill.consigneeUnit || ''}`,
date: `${this.localWaybill.deliveryYear || ''}${this.localWaybill.deliveryMonth || ''}${this.localWaybill.deliveryDay || ''}`,
sender: `发货单位:${this.localWaybill.senderUnit || ''}`
};
const header2 = { // 负责人+电话+合同号+车牌
principal: `负责人:${this.localWaybill.principal || ''}`,
phone: `电话:${this.localWaybill.principalPhone || ''}`,
contract: `合同号:${this.localWaybill.contractCode || ''}`,
license: `车牌:${this.localWaybill.licensePlate || ''}`
};
const tableHeader = ["品名", '切边', '包装', '仓库位置', '结算', '原料厂家', '卷号', '规格', '材质', '重量(t)', '备注']; // 表格表头
const tableData = this.displayWaybillDetails.map(item => [ // 表格明细
item.productName || '',
item.edgeType || '',
item.packageType || '',
item.actualWarehouseName || '',
item.settlementType || '',
item.rawMaterialFactory || '',
item.coilNumber || '',
item.specification || '',
item.material || '',
item.weight || '',
// item.unitPrice || '',
item.remark || ''
]);
const remark = '1、品名冷硬钢卷酸连轧、冷轧钢卷脱脂退火火拉矫、镀锌卷板镀锌管料镀锌分剪料2、切边净边/毛边3、包装裸包周三径四简包1周三径四内外护角简包2周三径四+防锈纸;普包:周三径四+内外护角+防锈纸+端护板精包1周三径四+内外护角+防锈纸+薄膜+端护板+内外护板精包2周三径四+内外护角+防锈纸+薄膜+端护板+内外护板+木托。';
const pickupLocation = `取货地点:${this.localWaybill.pickupLocation || ''}`;
const footer = { // 签名栏
deliveryman: `发货:${this.localWaybill.deliveryman || ''}`,
driver: `司机:${this.localWaybill.driver || ''}`,
weightRoom: `磅房:${this.localWaybill.weightRoom || ''}`
};
// 3. 写入数据到工作表(按行写入)
let rowIdx = 1; // exceljs行号从1开始
// 3.1 标题行第1行
const titleCell = worksheet.getCell(`A${rowIdx}`);
titleCell.value = title;
worksheet.mergeCells(`A${rowIdx}:K${rowIdx}`); // 合并A1-K1
// 3.2 收货单位行第2行
rowIdx++;
worksheet.getCell(`A${rowIdx}`).value = header1.consignee;
worksheet.getCell(`E${rowIdx}`).value = header1.date;
worksheet.getCell(`I${rowIdx}`).value = header1.sender;
worksheet.mergeCells(`A${rowIdx}:D${rowIdx}`); // 收货单位A2-D2
worksheet.mergeCells(`E${rowIdx}:H${rowIdx}`); // 日期E2-H2
worksheet.mergeCells(`I${rowIdx}:K${rowIdx}`); // 发货单位I2-L2
// 3.3 负责人行第3行
rowIdx++;
worksheet.getCell(`A${rowIdx}`).value = header2.principal;
worksheet.getCell(`D${rowIdx}`).value = header2.phone;
worksheet.getCell(`G${rowIdx}`).value = header2.contract;
worksheet.getCell(`J${rowIdx}`).value = header2.license;
worksheet.mergeCells(`A${rowIdx}:C${rowIdx}`); // 负责人A3-C3
worksheet.mergeCells(`D${rowIdx}:F${rowIdx}`); // 电话D3-F3
worksheet.mergeCells(`G${rowIdx}:I${rowIdx}`); // 合同号G3-I3
worksheet.mergeCells(`J${rowIdx}:K${rowIdx}`); // 车牌J3-L3
// 3.4 表格表头第4行
rowIdx++;
tableHeader.forEach((text, colIdx) => {
const colLetter = String.fromCharCode(65 + colIdx); // A=65, B=66...
worksheet.getCell(`${colLetter}${rowIdx}`).value = text;
});
// 3.5 表格明细行
tableData.forEach(item => {
rowIdx++;
item.forEach((text, colIdx) => {
const colLetter = String.fromCharCode(65 + colIdx);
worksheet.getCell(`${colLetter}${rowIdx}`).value = text;
});
});
// 3.6 备注行
rowIdx++;
const remarkCell = worksheet.getCell(`A${rowIdx}`);
remarkCell.value = remark;
worksheet.mergeCells(`A${rowIdx}:K${rowIdx}`); // 合并A*_L*
// 3.7 取货地点行
rowIdx++;
const pickupCell = worksheet.getCell(`A${rowIdx}`);
pickupCell.value = pickupLocation;
worksheet.mergeCells(`A${rowIdx}:K${rowIdx}`); // 合并A*_L*
// 3.8 签名栏行
rowIdx++;
worksheet.getCell(`A${rowIdx}`).value = footer.deliveryman;
worksheet.getCell(`E${rowIdx}`).value = footer.driver;
worksheet.getCell(`I${rowIdx}`).value = footer.weightRoom;
worksheet.mergeCells(`A${rowIdx}:D${rowIdx}`); // 发货A*_D*
worksheet.mergeCells(`E${rowIdx}:H${rowIdx}`); // 司机E*_H*
worksheet.mergeCells(`I${rowIdx}:K${rowIdx}`); // 磅房I*_L*
// 4. 配置列宽完全匹配Web端
const columnWidths = [70, 60, 50, 90, 70, 70, 150, 90, 80, 70, 60];
worksheet.columns = columnWidths.map((width, idx) => ({
key: `col${idx + 1}`,
width: width / 5 // exceljs的width单位是字符宽度转换为px约1px=0.072字符)
}));
// 5. 配置样式核心匹配Web端
// 工具函数:设置单元格样式
const setCellStyle = (cell, options = {}) => {
// 默认样式:宋体、加粗、居中
cell.font = {
name: 'SimSun', // 宋体匹配Web端
size: options.size || 16, // 默认16px表格单元格
bold: options.bold ?? true // 默认加粗
};
cell.alignment = {
horizontal: options.horizontal || 'left', // 水平居中
vertical: options.vertical || 'middle', // 垂直居中
wrapText: options.wrapText || false // 自动换行
};
// 边框:表格单元格带边框,其他区域无边框
if (options.border) {
cell.border = {
top: { style: 'thin', color: { argb: 'FF000000' } },
bottom: { style: 'thin', color: { argb: 'FF000000' } },
left: { style: 'thin', color: { argb: 'FF000000' } },
right: { style: 'thin', color: { argb: 'FF000000' } }
};
} else {
cell.border = null; // 无边框
}
};
// 5.1 标题样式24px、宋体、加粗、居中、无边框
setCellStyle(worksheet.getCell('A1'), { size: 24, border: false, horizontal: 'center' });
// 5.2 头部信息样式18px、宋体、加粗、居中、无边框
setCellStyle(worksheet.getCell(`A2`), { size: 18, border: false });
setCellStyle(worksheet.getCell(`E2`), { size: 18, border: false });
setCellStyle(worksheet.getCell(`I2`), { size: 18, border: false });
setCellStyle(worksheet.getCell(`A3`), { size: 18, border: false });
setCellStyle(worksheet.getCell(`D3`), { size: 18, border: false });
setCellStyle(worksheet.getCell(`G3`), { size: 18, border: false });
setCellStyle(worksheet.getCell(`J3`), { size: 18, border: false });
// 5.3 表格表头样式16px、宋体、加粗、居中、带边框
for (let col = 0; col < 11; col++) {
const colLetter = String.fromCharCode(65 + col);
setCellStyle(worksheet.getCell(`${colLetter}4`), { size: 16, border: true, horizontal: 'center' });
}
// 5.4 表格明细样式16px、宋体、加粗、居中、带边框
for (let r = 5; r < rowIdx - 2; r++) { // 明细行范围第5行到备注行前一行
for (let col = 0; col < 11; col++) {
const colLetter = String.fromCharCode(65 + col);
setCellStyle(worksheet.getCell(`${colLetter}${r}`), { size: 16, border: true, horizontal: 'center' });
}
}
// 5.5 备注样式18px、宋体、加粗、居中、自动换行、无边框
setCellStyle(remarkCell, { size: 18, border: false, wrapText: true });
// 5.6 取货地点样式18px、宋体、加粗、居中、无边框
setCellStyle(pickupCell, { size: 18, border: false });
// 5.7 签名栏样式18px、宋体、加粗、居中、无边框
setCellStyle(worksheet.getCell(`A${rowIdx}`), { size: 18, border: false });
setCellStyle(worksheet.getCell(`E${rowIdx}`), { size: 18, border: false });
setCellStyle(worksheet.getCell(`I${rowIdx}`), { size: 18, border: false });
// 第一行的行高
const row1 = worksheet.getRow(1);
row1.height = 50;
const row2 = worksheet.getRow(2);
row2.height = 40;
const row3 = worksheet.getRow(3);
row3.height = 40;
// 备注行的行高
const remarkRow = worksheet.getRow(12);
remarkRow.height = 100;
const pickupRow = worksheet.getRow(13);
pickupRow.height = 40;
const footerRow = worksheet.getRow(14);
footerRow.height = 40;
// 6. 生成Excel文件并下载
const buffer = await workbook.xlsx.writeBuffer(); // 生成二进制buffer
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `科伦普发货单_${this.waybill.waybillNo || Date.now()}.xlsx`;
document.body.appendChild(link);
link.click();
// 清理资源
setTimeout(() => {
document.body.removeChild(link);
URL.revokeObjectURL(url);
}, 100);
} catch (error) {
console.error('Excel导出失败', error);
this.$message.error('导出失败,请重试');
}
}
}
}
</script>
<style scoped>
.waybill-container {
width: 241mm;
height: 140mm;
margin: 0 auto;
padding: 0;
background: #fff;
box-shadow: none;
font-family: SimSun, "Courier New", monospace;
overflow: hidden;
color: #000 !important;
font-weight: 900 !important;
box-sizing: border-box;
}
.title {
text-align: center;
font-size: 24px;
font-weight: bold;
margin-bottom: 5px;
}
.waybill-content {
--paper-width-mm: 241;
--content-width-mm: 241;
--offset-x-mm: 0;
--offset-y-mm: 0;
--scale: calc(var(--content-width-mm) / var(--paper-width-mm));
width: calc(var(--paper-width-mm) * 1mm);
transform-origin: top left;
transform: translate(calc(var(--offset-x-mm) * 1mm), calc(var(--offset-y-mm) * 1mm)) scale(var(--scale));
}
.waybill-content * {
box-sizing: border-box;
}
/* 头部样式 */
.waybill-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 3mm;
font-size: 11px;
}
.header-left,
.header-right {
display: flex;
align-items: center;
}
.header-center {
display: flex;
align-items: center;
gap: 5px;
}
.label {
font-weight: bold;
display: inline-block;
width: 100px;
font-size: 18px;
text-align: right;
white-space: nowrap;
}
.date-label {
width: 1em;
}
/* 可编辑输入框样式 */
.editable-input {
padding: 4px 8px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 18px;
font-weight: bold;
font-family: SimSun, serif;
outline: none;
transition: all 0.2s;
line-height: 18px;
border-bottom: 1px dashed #dcdfe6;
}
.editable-input:focus {
border-color: #409eff;
}
.date-input {
width: 40px;
text-align: center;
margin-right: 5px;
}
/* 透明输入框样式 */
.transparent-input {
border: none;
border-radius: 0;
background-color: transparent;
}
.transparent-input:focus {
border-bottom-color: #409eff;
background-color: rgba(64, 158, 255, 0.05);
}
/* 表格样式 */
.waybill-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 2mm;
font-size: 10px;
table-layout: fixed;
}
.waybill-table th,
.waybill-table td {
border: 0.4px solid #000;
box-sizing: border-box;
line-height: 6mm;
text-align: center;
vertical-align: middle;
font-size: 16px;
font-weight: 900;
padding: 0;
overflow: hidden;
white-space: nowrap;
}
/* 表格列宽设置 */
.waybill-table th:nth-child(1),
.waybill-table td:nth-child(1) {
width: 70px;
/* 品名 */
}
.waybill-table th:nth-child(2),
.waybill-table td:nth-child(2) {
width: 60px;
/* 切边 */
}
.waybill-table th:nth-child(3),
.waybill-table td:nth-child(3) {
width: 50px;
/* 包装 */
}
.waybill-table th:nth-child(4),
.waybill-table td:nth-child(4) {
width: 90px;
/* 仓库位置 */
}
.waybill-table th:nth-child(5),
.waybill-table td:nth-child(5) {
width: 80px;
/* 结算 */
}
.waybill-table th:nth-child(6),
.waybill-table td:nth-child(6) {
width: 70px;
/* 原料厂家 */
}
.waybill-table th:nth-child(7),
.waybill-table td:nth-child(7) {
width: 150px;
/* 卷号 */
}
.waybill-table th:nth-child(8),
.waybill-table td:nth-child(8) {
width: 90px;
/* 规格 */
}
.waybill-table th:nth-child(9),
.waybill-table td:nth-child(9) {
width: 80px;
/* 材质 */
}
/* 数量(件) */
/* .waybill-table th:nth-child(10),
.waybill-table td:nth-child(10) {
width: 60px;
} */
/* 重量kg */
.waybill-table th:nth-child(10),
.waybill-table td:nth-child(10) {
width: 70px;
}
/* 单价 */
/* .waybill-table th:nth-child(11),
.waybill-table td:nth-child(11) {
width: 50px;
} */
/* 备注 */
.waybill-table th:nth-child(11),
.waybill-table td:nth-child(11) {
/* width: 40px; */
}
.waybill-table th {
background-color: #f5f7fa;
font-weight: bold;
}
.waybill-table tr:nth-child(even) {
background-color: #fafafa;
}
/* 表格输入框样式 */
.table-input {
box-sizing: border-box;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
border: none;
outline: none;
background: transparent;
/* font-family: inherit; */
font-size: 16px;
line-height: 6mm;
text-align: center;
vertical-align: middle;
}
.transparent-input {
border: none !important;
background: transparent !important;
padding: 0 !important;
margin: 0 !important;
height: 100% !important;
line-height: 6mm !important;
vertical-align: middle !important;
}
.waybill-table th,
.waybill-table td {
padding: 0 !important;
margin: 0 !important;
line-height: 6mm !important;
height: 6mm;
vertical-align: middle !important;
}
.waybill-table input {
vertical-align: middle !important;
height: 6mm !important;
line-height: 6mm !important;
padding: 0 2px !important;
margin: 0 !important;
border: none !important;
background: transparent !important;
box-shadow: none !important;
width: 100% !important;
max-width: 100% !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
.waybill-table input:focus {
outline: none !important;
box-shadow: none !important;
border: 1px dashed #999 !important;
}
/* 无数据样式 */
.no-data {
height: 200px;
vertical-align: middle;
}
.no-data-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
/* 表格单元格操作区 */
.table-cell-with-action {
display: flex;
align-items: center;
gap: 5px;
}
/* 备注样式 */
.waybill-remarks {
margin-bottom: 30px;
font-size: 18px;
line-height: 1.5;
/* font-weight: 600; */
font-weight: bold;
text-align: justify;
}
.waybill-remarks p {
margin: 5px 0;
}
/* 底部签名样式 */
.waybill-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
font-size: 18px;
}
.waybill-pickup-location {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-size: 18px;
}
.waybill-pickup-location label {
font-size: 18px;
/* margin-right: 10px; */
text-align: left !important;
width: 40px;
}
.footer-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
width: 200px;
}
.footer-item.inline {
flex-direction: row;
align-items: center;
width: auto;
}
.footer-item .label {
font-size: 18px;
margin-right: 10px;
width: 40px;
}
.signature-input {
min-width: 150px;
}
.full-input {
/* 占满本行的剩余空间父容器不是flex */
flex: 1;
}
/* 操作按钮样式 */
.waybill-actions {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 30px;
}
/* 添加明细按钮 */
.add-detail-btn {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
/* 打印样式 */
@page {
size: 241mm 140mm;
margin: 0;
}
@media print {
html,
body {
margin: 0;
padding: 0;
}
/* 打印机通常存在不可打印区这里右侧预留30mm安全边距避免右侧被裁 */
.waybill-container {
width: 241mm;
height: 140mm;
margin: 0;
padding: 0;
overflow: hidden;
}
.waybill-content {
--paper-width-mm: 241;
/* 右侧预留约30mm不可打印区241 - 30 = 211 */
--content-width-mm: 211;
--offset-x-mm: 0;
--offset-y-mm: 0;
--scale: calc(var(--content-width-mm) / var(--paper-width-mm));
width: calc(var(--paper-width-mm) * 1mm);
transform-origin: top left;
transform: translate(calc(var(--offset-x-mm) * 1mm), calc(var(--offset-y-mm) * 1mm)) scale(var(--scale));
}
/* 打印时不要省略:允许换行并让行高/高度自适应 */
.waybill-table th,
.waybill-table td {
white-space: normal !important;
overflow: visible !important;
text-overflow: clip !important;
height: auto !important;
line-height: 1.2 !important;
}
.waybill-table input {
white-space: normal !important;
overflow: visible !important;
text-overflow: clip !important;
}
.waybill-footer {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.footer-item.inline {
width: 100%;
}
.label {
width: auto;
text-align: left;
}
}
</style>